springboot应用-shiro增强权限管理

    技术2026-03-08  8

    本文实现了基于shiro、mybatis-plus、thymeleaf、vue、axios、hutools的基本权限管理demo,提供了用户登陆、注册、查看、锁定\解锁以及excel导出功能

    基本功能

    本文是在上一篇shiro简单权限管理的基础上,实现:

    基于RBAC权限模型,建立相关数据库表,实现shiro框架与数据库的对接。基于mybatis-plus,实现相关权限查询、用户认证、新增注册用户等功能。基于thymleaf、vue、axios实现简单的前端页面展示、交互。

    数据模型构建

    首先,需要分别建立以下五张表,分别:

    编码名称备注t_user用户表存储用户基本信息,例如张三、李四、王五等t_role角色表存储角色基本信息,例如普通角色user,管理员角色admint_permission权限表存储权限信息,例如查看用户信息、锁定用户等t_user_role_rel用户角色关系表存储用户所属角色,支撑用户与角色的多对多关系(一个用户可拥有多个角色,一个角色可分配到多个用户)t_role_permission_rel角色权限关系表存储角色拥有的操作权限,支撑角色与权限的多对多关系

    角色建表语句如下:

    drop table if exists t_user; drop table if exists t_role; drop table if exists t_authority; drop table if exists t_user_role_rel; drop table if exists t_role_authority_rel; create table t_user ( id bigint(20) comment '用户id', name varchar(64) comment '用户账号', nick_name varchar(32) comment '用户名称', password varchar(32) comment '加密密码', salt varchar(32) comment '密码盐值', state int(1) comment '用户状态', create_time timestamp comment '创建时间', update_time timestamp comment '更新时间', primary key (id), unique key (name) ); create table t_role ( id bigint(20) comment '角色id', name varchar(32) comment '角色名称', code varchar(32) comment '角色编码', create_time timestamp comment '创建时间', update_time timestamp comment '更新时间', primary key (id) ); create table t_permission ( id bigint(20) comment '权限id', parent_id bigint(20) comment '父权限id', name varchar(32) comment '权限名称', type varchar(10) comment '权限类型', code varchar(32) comment '权限编码', create_time timestamp comment '创建时间', update_time timestamp comment '更新时间', primary key (id) ); create table t_user_role_rel ( id bigint(20) comment '用户角色关系id', user_id bigint(20) comment '用户id', role_id bigint(20) comment '角色id', primary key (id) ); create table t_role_permission_rel ( id bigint(20) comment '角色权限关系id', role_id bigint(20) comment '角色id', permission_id bigint(20) comment '权限id', primary key (id) );

    依赖引入

    本demo主要涉及到如下依赖:

    <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-web-starter</artifactId> <version>1.5.3</version> </dependency> <dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.0.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.72</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.3.7</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>4.1.2</version> </dependency>

    后台数据模型服务编写

    此部分主要是基于mybatis-plus,来实现相关的entity、mapper和service:

    Entity实体类编写

    具体代码如下:

    @TableName("t_user") @Data @Builder public class User { private Long id; private String name; private String nickName; private String password; private String salt; private UserStateEnum state; private LocalDateTime createTime; private LocalDateTime updateTime; }

    因为要对用户状态进行管理,还需要实现用户状态枚举类,并且通过@EnumValue来申明与mybatis-plus对应的数据库枚举字段值:

    @Getter public enum UserStateEnum { /** 锁定状态 */ LOCKED(0, "锁定"), /** 正常状态 */ NORMAL(1, "正常"); @EnumValue private final int key; private final String desc; UserStateEnum(int key, String desc) { this.key = key; this.desc = desc; } }

    用户角色关系实体:

    @TableName("t_user_role_rel") @Data @Builder public class UserRole { private Long id; private Long userId; private Long roleId; }

    角色实体:

    @TableName("t_role") @Data public class Role { private Long id; private String name; private String code; private LocalDateTime createTime; private LocalDateTime updateTime; }

    角色权限关系实体:

    @TableName("t_role_permission_rel") @Data public class RolePermission { @TableId private Long id; private Long roleId; private Long permissionId; }

    权限实体:

    @TableName("t_permission") @Data public class Permission { private Long id; private Long parentId; private String name; private String type; private String code; private LocalDateTime createTime; private LocalDateTime updateTime; }

    Mapper接口编写

    UserMapper类没有特殊方法,直接继承即可:

    public interface UserMapper extends BaseMapper<User> { }

    用户角色UserRoleMapper:

    public interface UserRoleMapper extends BaseMapper<UserRole> { }

    RoleMapper因为要通过用户id查询用户的所有角色,满足shiro配置,因此增加了一个查询方法(该方法内容在对应的xml文件中,具体见后文):

    public interface RoleMapper extends BaseMapper<Role> { /** * 根据用户id查询用户角色清单 * * @param userId * @return */ public List<Role> selectUserRoles(Long userId); } public interface RolePermissionMapper extends BaseMapper<RolePermission> { }

    同样,PermissionMapper因为要查询用户的全部权限,也实现了一个查询方法:

    public interface PermissionMapper extends BaseMapper<Permission> { /** * 根据用户id查询用户权限清单 * * @param userId * @return */ public List<Permission> selectUserPermissions(Long userId); }

    XML Mapper编写

    一般推荐的做法是将sql写到xml配置文件中,以便进行格式化等操作。与上面mapper接口对应的两个mapper文件如下:

    <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="pers.techlmm.shiro.advanced.mapper.RoleMapper"> <select id="selectUserRoles" parameterType="long" resultType="pers.techlmm.shiro.advanced.entity.Role"> select r.* from t_user_role_rel ur, t_role r where ur.role_id = r.id and ur.user_id = #{userId} </select> </mapper> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="pers.techlmm.shiro.advanced.mapper.PermissionMapper"> <select id="selectUserPermissions" parameterType="long" resultType="pers.techlmm.shiro.advanced.entity.Permission"> select p.* from t_user_role_rel ur, t_role_permission_rel rp, t_permission p where ur.role_id = rp.role_id and p.id = rp.permission_id and ur.user_id = #{userId} </select> </mapper>

    Service服务类编写

    完成了数据库层面的准备,接下来是提供面向上层应用的服务了。

    首先是实现了UserBusiService,也就用户业务服务,实现获取全部用户信息,并封装为BO对象,具体如下:

    @Service public class UserBusiService { @Autowired private UserMapper userMapper; @Autowired private RoleMapper roleMapper; @Autowired private PermissionMapper permissionMapper; @Autowired private UserRoleMapper userRoleMapper; @Autowired private RoleService roleService; @Autowired private Snowflake snowflake; /** * 获取所有用户基本信息,含所属角色及权限清单 * * @return */ public List<UserBO> getUserBOList() { List<UserBO> userBOList = new ArrayList<>(); userMapper.selectList(null).forEach(user -> { // 遍历每一个用户,并封装为BO对象 UserBO userBO = UserBO.builder() .id(user.getId()) .name(user.getName()) .nickName(user.getNickName()) .createTime(user.getCreateTime()) .state(user.getState()) .build(); // 设置用户的角色集合 userBO.setRoles(this.getUserRoleSet(user.getId())); // 设置用户的权限集合 userBO.setPermissions(this.getUserPermissionSet(user.getId())); userBOList.add(userBO); }); return userBOList; } public Set<String> getUserRoleSet(Long userId) { // 基于stream操作,将每个Role对象,取出code后,归并为set return roleMapper.selectUserRoles(userId) .stream() .map(Role::getCode) .collect(Collectors.toSet()); } public Set<String> getUserPermissionSet(Long userId) { // 基于stream操作,将每个权限对象,归并为权限code的集合 return permissionMapper.selectUserPermissions(userId) .stream() .map(Permission::getCode) .collect(Collectors.toSet()); } public User getUserInfo(String name) { // 按名称查询用户,返回一个结果 QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("name", name); return userMapper.selectOne(wrapper); } /** * 添加用户,默认为普通user角色 * * @param user * @return */ @Transactional(rollbackFor = Exception.class) public User addUser(User user) { Role role = roleService.getRoleByCode("user"); if (role == null) { throw new RuntimeException("以普通用户角色创建用户出现异常"); } return this.addUser(user, role); } /** * 添加用户,并赋予指定的角色 * * @param user * @param role * @return */ @Transactional(rollbackFor = Exception.class) public User addUser(User user, Role role) { // 先添加用户主对象 user.setCreateTime(LocalDateTime.now()); user.setUpdateTime(LocalDateTime.now()); user.setState(UserStateEnum.NORMAL); // 密码盐值为 随机8位字符 String salt = RandomUtil.randomString(8); user.setSalt(salt); // 下面的参数设定,如算法、迭代次数,要与shiro配置一致 SimpleHash hash = new SimpleHash(Md5Hash.ALGORITHM_NAME, user.getPassword(), ByteSource.Util.bytes(salt), 1024); // 将密码设置为加密后的base64字符串 user.setPassword(hash.toBase64()); user.setId(snowflake.nextId()); int result = userMapper.insert(user); if (result > 0) { // 插入用户所属角色 UserRole userRole = UserRole.builder() .id(snowflake.nextId()) .roleId(role.getId()) .userId(user.getId()) .build(); result += userRoleMapper.insert(userRole); } if (result < 2) { throw new RuntimeException("添加新注册用户出现异常"); } return user; } }

    对应的BO对象定义如下:

    @Data @Builder @AllArgsConstructor @NoArgsConstructor public class UserBO { private Long id; private String name; private String nickName; private UserStateEnum state; private LocalDateTime createTime; private Set<String> roles; private Set<String> permissions; }

    鉴于锁定、解锁操作是针对用户本身,将该功能实现在UserService中,而不纳入到UserBusiService:

    @Service public class UserService { @Autowired private UserMapper userMapper; @Transactional(rollbackFor = Exception.class) public int lockUser(long id) { User user = userMapper.selectById(id); if (user == null) { throw new RuntimeException("锁定操作异常,用户id不存在:" + id); } // 设置用户状态 UserStateEnum stateEnum = user.getState() == UserStateEnum.LOCKED ? UserStateEnum.NORMAL : UserStateEnum.LOCKED; user.setState(stateEnum); user.setUpdateTime(LocalDateTime.now()); return userMapper.updateById(user); } }

    另外,因为新注册用户要分配默认权限,实现如下的基于code查询权限的服务:

    @Service public class RoleService { @Autowired private RoleMapper roleMapper; public Role getRoleByCode(String code) { QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("code", code); return roleMapper.selectOne(wrapper); } }

    后台web功能编写

    提供了相关service服务后,接下来是着手实现相关controller等web服务功能了。

    控制器编写

    首先是LoginController,实现:

    实现doLogin逻辑,完成用户登陆。实现doRegister逻辑,完成新用户注册。 @Controller @Slf4j public class LoginController { @Autowired private UserBusiService userBusiService; @PostMapping("/doLogin") public String doLogin(String username, String password, String strRememberMe, Model model) { boolean rememberMe = "on".equals(strRememberMe); UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe); Subject subject = SecurityUtils.getSubject(); subject.login(token); return "redirect:/user/all"; } @RequestMapping("/doRegister") @ResponseBody public CommonResult<User> doRegister(@RequestBody String form) { JSONObject jsonForm = JSON.parseObject(form); String userName = jsonForm.getString("username"); String nickName = jsonForm.getString("nickname"); String password = jsonForm.getString("password"); User user = User.builder().name(userName).nickName(nickName).password(password).build(); user = userBusiService.addUser(user); if (user == null) { return CommonResult.failed(); } else { return CommonResult.success(user); } } }

    接下来是实现 UserController:

    实现getAllUser,查询所有用户信息,并封装为可供前端展示的BO对象列表。实现doLock逻辑,实现对用户的解锁、锁定操作,并且通过@RequiresRoles等注解,对该api进行权限管理。实现doExport逻辑,基于hutools的工具类,以及apache-poi,实现简单的用户列表导出excel。 @Controller @RequestMapping("/user") @Slf4j public class UserController { @Autowired private UserBusiService userBusiService; @Autowired private UserService userService; @RequestMapping("/all") public String getAllUser(Model model) { List<UserBO> users = userBusiService.getUserBOList(); model.addAttribute("users", users); return "advance/user-list"; } @RequiresRoles("admin") @RequiresPermissions({"userInfo:lock", "userInfo:unlock"}) @RequestMapping("/lock") public String doLock(@RequestParam Long id) { userService.lockUser(id); return "redirect:/user/all"; } @RequestMapping("/exp") public void doExport(HttpServletRequest request, HttpServletResponse response) throws IOException { ExcelWriter writer = ExcelUtil.getWriter(true); List<UserBO> users = userBusiService.getUserBOList(); writer.write(users, true); response.setContentType( "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"); response.setHeader("Content-Disposition", "attachment;filename=users.xlsx"); ServletOutputStream outputStream = response.getOutputStream(); writer.flush(outputStream, true); writer.close(); IoUtil.close(outputStream); } }

    异常处理类编写

    为了集中处理运行时异常,实现了如下的异常类:

    @ControllerAdvice public class AppExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(AuthenticationException.class) public ModelAndView handleAuthenticationException(AuthenticationException ex) { ModelAndView mv = new ModelAndView("advance/login"); mv.addObject("error", ex.getMessage()); logger.warn(ex); return mv; } }

    基于上述异常类,因此在shiro的login出现AuthenticationException时,不需要在LoginController的doLogin中进行try catch处理,在上面集中处理集合。上文基本等同于控制器中的如下代码:

    Subject subject = SecurityUtils.getSubject(); String loginError = ""; try { // 执行登陆操作 subject.login(token); } catch (UnknownAccountException | IncorrectCredentialsException ex) { loginError = "用户名或密码错误"; log.warn("{}", loginError, ex); } catch (LockedAccountException ex) { loginError = "用户账号被锁定"; log.warn("{}", loginError, ex); } catch (AuthenticationException ex) { loginError = "用户账号暂不可用"; log.warn("{}", loginError, ex); } if (!loginError.isEmpty()) { model.addAttribute("error", loginError); // 登陆失败,回到登陆页 return "advance/login"; }

    配置类实现

    配置文件对于不属性框架来说,很容易因为错漏导致各种问题,此处也许特别注意。

    Shiro相关配置类

    首先实现自己的UserRealm类,实现鉴权和验证信息的获取:

    public class UserRealm extends AuthorizingRealm { @Autowired private UserBusiService userBusiService; /** * 获取用户鉴权信息,也即设置用户角色和权限 * * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); User user = (User) getAvailablePrincipal(principals); Long userId = user.getId(); authorizationInfo.setRoles(userBusiService.getUserRoleSet(userId)); authorizationInfo.setStringPermissions(userBusiService.getUserPermissionSet(userId)); return authorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken = (UsernamePasswordToken) token; String username = upToken.getUsername(); User user = userBusiService.getUserInfo(username); if (user == null) { throw new UnknownAccountException("用户账号不存在"); } if (user.getState() == UserStateEnum.LOCKED) { throw new LockedAccountException("用户账号被锁定"); } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName()); return authenticationInfo; } }

    接下来是,实现:

    实例化HashedCredentialsMatcher,定义相关加密算法等信息。实例化Realm,特别注意要将上述HashedCredentialsMatcher的也设置到该示例中。定义ShiroFilterChainDefinition,添加相关URL拦截规则。实例化ShiroDialect,因为前段页面有用到基于thymeleaf-extras-shiro的权限便签,例如shiro:hasAnyPermissions实例化DefaultAdvisorAutoProxyCreator,从相关资料看是为了解决相关bug,具体没验证

    相关代码如下:

    @Configuration public class ShiroConfig { @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); matcher.setHashAlgorithmName(Md5Hash.ALGORITHM_NAME); matcher.setHashIterations(1024); // 按base64模式 matcher.setStoredCredentialsHexEncoded(false); return matcher; } @Bean public Realm realm() { UserRealm realm = new UserRealm(); // 主要要手工设置一下 realm.setCredentialsMatcher(hashedCredentialsMatcher()); return realm; } @Bean public static DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator(); creator.setUsePrefix(true); return creator; } @Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() { DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition(); chain.addPathDefinition("/login", "anon"); chain.addPathDefinition("/doLogin", "anon"); chain.addPathDefinition("/register", "anon"); chain.addPathDefinition("/doRegister", "anon"); // 静态资源不拦截 chain.addPathDefinition("/js/**", "anon"); chain.addPathDefinition("/css/**", "anon"); chain.addPathDefinition("/logout", "logout"); // 相关 filter参见 https://shiro.apache.org/web.html chain.addPathDefinition("/**", "user"); return chain; } @Bean public ShiroDialect shiroDialect() { return new ShiroDialect(); } @Bean protected CacheManager cacheManager() { return new MemoryConstrainedCacheManager(); } }

    Mybatis-Plus配置类

    该类很简单,就是定义了mapper的扫描路径:

    @Configuration @MapperScan("pers.techlmm.shiro.advanced.mapper") public class MybatisPlusConfig { }

    其他配置类

    首先是实现本dmeo的WebConfig:

    添加基于FastJsonHttpMessageConverter的messageConverters,因为基本都是采用非restful模式,但注册用户是采用的responsebody模式,通过该converter实现doRegister返回结果中的CommonResult实体正常转换为json对象返回前端。通过addViewControllers,添加login和register两个前端视图,从而无需实现无业务含义的请求转发。 @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/login").setViewName("advance/login"); registry.addViewController("/register").setViewName("advance/register"); } @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter(); converters.add(converter); } }

    其次,为了实现基于hutools的snowflake id生成,实现了如下AppConfig配置类:

    @Configuration public class AppConfig { @Bean public Snowflake snowflake() { return IdUtil.createSnowflake(1, 1); } }

    通用类实现

    最后,为了实现通过结果的返回,实现了如下的CommonResult和ResultCode

    @AllArgsConstructor @Getter public class CommonResult<T> { private int code; private String message; private T data; public static <T> CommonResult<T> success(T data) { return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getValue(), data); } public static <T> CommonResult<T> success(T data, String message) { return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data); } public static <T> CommonResult<T> failed() { return new CommonResult<T>(ResultCode.FAILED.getCode(), ResultCode.FAILED.getValue(), null); } public static <T> CommonResult<T> failed(String message) { return new CommonResult<T>(ResultCode.FAILED.getCode(), message, null); } } @Getter public enum ResultCode { /** 操作成功 */ SUCCESS(200, "操作成功"), /** 操作失败 */ FAILED(500, "操作失败"); private int code; private String value; ResultCode(int code, String value) { this.code = code; this.value = value; } }

    application配置文件

    本demo是基于yaml来实现配置,并且将所有该demo的配置都写到application-ashiro.yml中,只在application.yml中指定active profile:

    spring: profiles: active: ashiro

    application-ashiro.yml具体内容如下,主要配置了:

    初始化数据库脚本thymeleaf模板文件不缓存设置了context-path设置了shiro相关的loginUrl和successUrl最后,特别需要说明,上面UserStateEnum类,需要在mybatis-plus中手工配置一下enums的包路径。 spring: datasource: schema: classpath:db/shiro/schema.sql data: classpath:db/shiro/data.sql url: jdbc:h2:mem:test username: root password: test thymeleaf: cache: false server: port: 8080 servlet: context-path: /api/v1 shiro: loginUrl: /login successUrl: /user/all mybatis-plus: type-enums-package: pers.techlmm.shiro.advanced.entity.enums

    前端HTML模板文件实现

    实现完所有后端功能后,就是编写前端HTML文件了。

    login.html实现

    通过该文件实现用户登陆,要点如下:

    form表单的action,可以采用th标签及thymeleaf@url模式,实现自动管理baseurl,例如 th:action="@{/doLogin}",因为我在后端配置了统一的context url 为/api/v1,采用该模式可以不耦合url。实现一个不太完善的rememberMe功能。 <!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>登录页</title> </head> <body> <h3>请登录</h3> <form th:action="@{/doLogin}" method="post"> <div th:text="${error}"></div> <input type="text" name="username" placeholder="用户名称"/><br/> <input type="password" name="password" placeholder="登录密码"/><br/> <input type="checkbox" name="rememberMe" id="remCheck"/><label for="remCheck">记住我</label><br/> <input type="submit" value="登录"/> <a th:href="@{/register}">注册</a> </form> </body> </html>

    register.html实现

    在该文件中,实现了基于vue和axios的数据操作和交互,要点如下:

    引入vue.js文件时,需要基于thymeleaf规范,例如th:src="@{/js/vue.js}",vue.js文件防止到resources/static下,才能被访问到,并且要在shiro的filter中放开类似/js/**的拦截。基于axios提交请求时,主要要设置baseURL,例如axios.defaults.baseURL = 'http://localhost:8080/api/v1';因为form中submit默认会提交并刷新页面,所以要通过@submit.prevent="doSubmit"来设定响应方法并阻止事件的传递当前是在axios请求的响应中,通过response.status === 200 && response.data.code === 200里判断注册成功,然后通过window.location.href = '/api/v1/login'来实现跳转到登陆页

    全部代码如下:

    <!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>注册用户</title> <script type="text/javascript" th:src="@{/js/vue.js}"></script> <!--<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>--> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> </head> <body> <h3>注册新用户</h3> <div id="app"> <span style="color: red">{{error}}</span> <form @submit.prevent="doSubmit"> 用户名称:<input type="text" placeholder="用户名称" v-model="username"/><br/> 用户昵称:<input type="text" placeholder="昵称" v-model="nickname"><br> 登录密码:<input type="password" placeholder="登录密码" v-model="password"/><br/> 确认密码:<input type="password" placeholder="再输入一次" v-model="password2"/><br/> <button type="submit">保存并提交</button> <button type="reset">清空表单</button> </form> </div> </body> <script> //<![CDATA[ axios.defaults.baseURL = 'http://localhost:8080/api/v1'; var app = new Vue({ el: "#app", data: { username: '', nickname: '', password: '', password2: '', error: '' }, methods: { doSubmit: function () { let errors = []; if (this.username.trim().length === 0) { errors.push("用户名称为空") } if (this.nickname.trim().length === 0) { errors.push("用户昵称为空"); } if (this.password.trim().length === 0) { errors.push("登陆密码为空"); } if (this.password.trim() !== this.password2.trim()) { errors.push("两次输入密码不一致"); } if (errors.length > 0) { this.error = errors.join(","); } else { axios.post('/doRegister', { username: this.username, password: this.password, nickname: this.nickname }) .then(function (response) { if (response.status === 200 && response.data.code === 200) { window.alert("注册成功,请登录"); window.location.href = '/api/v1/login'; } else { console.log(response); } }) .catch(function (error) { this.error = error; console.error(error); }) } } } }); //]]> </script> </html>

    user-list.html实现

    用户列表页面主要实现:

    通过<shiro:principal/>来展示当前用户信息通过<a th:href="@{/user/exp}">导出excel</a>来提供导出excel功能通过th:each="user:${users}"来实现对用户清单的遍历,并以table形式展示通过th:switch="${user.state}"和th:case="${T(pers.techlmm.shiro.advanced.entity.enums.UserStateEnum).NORMAL}"来实现对用户状态的枚举和判断通过shiro:hasAnyPermissions="userInfo:lock,userInfo:unlock"来实现锁定、解锁权限的前端控制通过<a th:href="@{/logout}">退出登录</a>提供退出登录功能

    全部代码如下:

    <!DOCTYPE html> <html lang="en" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>用户列表</title> </head> <body> <shiro:principal/> <a th:href="@{/user/exp}">导出excel</a> <table cellspacing="0" border="1"> <thead> <tr> <th>用户名称</th> <th>用户昵称</th> <th>用户角色</th> <th>用户权限</th> <th>用户状态</th> <th>注册时间</th> <th>操作</th> </tr> </thead> <tbody> <tr th:each="user:${users}"> <td th:text="${user.name}">张三</td> <td th:text="${user.nickName}">张三</td> <td th:text="${user.roles}"></td> <td th:text="${user.permissions}"></td> <td th:text="${user.state}"></td> <td th:text="${user.createTime}"></td> <td th:switch="${user.state}"> <a th:href="@{/user/lock(id=${user.id})}" shiro:hasAnyPermissions="userInfo:lock,userInfo:unlock"> <span th:case="${T(pers.techlmm.shiro.advanced.entity.enums.UserStateEnum).NORMAL}">锁定 </span> <span th:case="${T(pers.techlmm.shiro.advanced.entity.enums.UserStateEnum).LOCKED}">解锁</span> </a> </td> </tr> </tbody> </table> <a th:href="@{/logout}">退出登录</a> </body> </html>

    测试验证

    初始化数据

    首先,当前demo除实现了手工注册普通用户外,其他都需要初始化:

    初始化zhangsan、lisi两个用户。初始化user、admin两个角色。初始化用户查看、锁定用户、解锁用户三个权限。初始化两个关系表。

    具体初始化脚本如下:

    insert into t_user values (1, 'zhangsan', '张三', 'hroBDotL1aQX/I4ExjJ19Q==', 'c3ar6xy9', 1, now(), now()); insert into t_user values (2, 'lisi', '李四', '9iiqNp968bPRXlJVrTCiRw==', 'r8b18roi', 1, now(), now()); insert into t_role values (1, '普通用户', 'user', now(), now()); insert into t_role values (2, '管理员', 'admin', now(), now()); insert into t_permission values (1, 0, '用户管理', 'menu', 'userInfo:view', now(), now()); insert into t_permission values (2, 1, '锁定用户', 'button', 'userInfo:lock', now(), now()); insert into t_permission values (3, 1, '解锁用户', 'button', 'userInfo:unlock', now(), now()); insert into t_user_role_rel values (1, 1, 1); insert into t_user_role_rel values (2, 2, 2); insert into t_role_permission_rel values (1, 1, 1); insert into t_role_permission_rel values (2, 2, 1); insert into t_role_permission_rel values (3, 2, 2); insert into t_role_permission_rel values (4, 2, 3);

    上述初始化数据中,用户密码是经hello原始密码及随机盐值,经过md5加密后的,可通过如下代码来生成指定的数据:

    // 随机生成盐 String salt = RandomUtil.randomString(8); ByteSource bsalt = ByteSource.Util.bytes(salt); Object password = "hello"; SimpleHash hash = new SimpleHash(Md5Hash.ALGORITHM_NAME, password, bsalt, 1024); log.info("原始密码 {},密码盐值 {},加密密码 {}", password, salt, hash.toBase64());

    效果测试

    具体测试模式:

    1、启动服务后,访问http://localhost:8080/api/v1/login,进入登录页面

    2、以zhangsan/hello登录,可查看到 用户明细,操作列为空,具体如下图:

    3、以lisi登录,查看用户清单,并进行锁定、解锁操作:

    4、点击列表中的导出excel链接,测试用户导出情况:

    5、退出登录后,通过登录页,进入到注册页面,新注册一个用户王五: 6、以新注册王五登录后,查看用户清单:(可在下图中看到新注册用户具体信息)

    补充说明

    热部署调试配置

    为了实现在修改文件后,自动刷新而不需要重启,对于IDEA开发模式,可如下配置:

    引入 spring-boot-devtools依赖,并且设置spring-boot-maven-plugin的fork配置在application配置文件中,将thymeleaf的cache属性是指为false如果需要自动体现,可通过saveaction插件的build actions中的 compile files属性。如果不需要自动体现,可手工在修改了文件后,通过ctr+shift+f9重新编译当前文件,通过ctrl+f9重新编译整个工程来体现。

    具体pom.xml改动如下:

    <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> </configuration> </plugin> </plugins> </build>

    源代码下载

    本demo相关所有源代码,已经开放到码云,具体地址为 https://gitee.com/coolpine/backends ,供参考,欢迎反馈相关问题和意见。

    参考资料

    https://shiro.apache.org/spring-boot.htmlhttps://github.com/apache/shiro/tree/master/samples/spring-boot-webhttps://www.baeldung.com/spring-redirect-and-forwardhttps://github.com/theborakompanioni/thymeleaf-extras-shirohttps://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#link-urlshttps://www.baeldung.com/spring-thymeleaf-css-js
    Processed: 0.022, SQL: 9