飞道的博客

一看就懂!Springboot +Shiro +VUE 前后端分离式权限管理系统

1145人阅读  评论(0)

目录

前言

Let's do it!!

第一步:新建工程

第二步:准备好要用的包包和类类

第三步:编写登陆入口

第四步:编写ShiroService中的方法

第五步:编写ShiroConfig类

第六步:实现自定义的AuthenticationToken。

第七步:编写自己的Realm

第八步:实现自定义AuthenticatingFilter。

第九步:详解校验流程

看看效果

总结


前言

前段日子写过一篇关于SpringBoot+Shiro的简单整合的例子,那个例子并不适用于我们目前的前后端分离开发的趋势。我之前写过一个项目也是用到了Shiro的前后端分离,某度了许久也没找到解决方案,什么去掉shiroFilter.setLoginUrl();也阻止不了讨人厌的login.jsp的出现。直到我看到了renren-fast的源码...废话不多说,让我们来看看如何实现吧!

 前后端分离
要实现前后端分离,需要考虑以下2个问题: 1. 项目不再基于session了,如何知道访问者是谁? 2. 如何确认访问者的权限?


前后端分离,一般都是通过token实现,本项目也是一样;用户登录时,生成token及 token过期时间,token与用户是一一对应关系,调用接口的时候,把token放到header或 请求参数中,服务端就知道是谁在调用接口。

代码已上传到Git:

后台代码:https://github.com/FENGZHIJIE1998/shiro-auth 

前端代码:https://github.com/FENGZHIJIE1998/shiro-vue

觉得好用的记得点个Star哦

Let's do it!!


介绍:这次我们使用Shiro快速搭建前后端分离的权限管理系统 利用JPA帮我们管理数据库,Swagger Knife4j 帮我搭建Web测试环境;

后台基于 Springboot JPA Knife4j Shiro

前端基于 VUE ElementUI

注意:主要观察token的使用方法!

第一步:新建工程

pom文件application.yml巴拉巴拉这里省略,这里贴出需要用到的依赖:


  
  1. <!--starter-->
  2. <dependency>
  3. <groupId>org.springframework.boot </groupId>
  4. <artifactId>spring-boot-starter </artifactId>
  5. </dependency>
  6. <!-- test-->
  7. <dependency>
  8. <groupId>org.springframework.boot </groupId>
  9. <artifactId>spring-boot-starter-test </artifactId>
  10. <scope>test </scope>
  11. </dependency>
  12. <!--web-->
  13. <dependency>
  14. <groupId>org.springframework.boot </groupId>
  15. <artifactId>spring-boot-starter-web </artifactId>
  16. </dependency>
  17. <!--validation-->
  18. <dependency>
  19. <groupId>org.springframework.boot </groupId>
  20. <artifactId>spring-boot-starter-validation </artifactId>
  21. </dependency>
  22. <!--JPA-->
  23. <dependency>
  24. <groupId>org.springframework.boot </groupId>
  25. <artifactId>spring-boot-starter-data-jpa </artifactId>
  26. </dependency>
  27. <!--JDBC-->
  28. <dependency>
  29. <groupId>org.springframework.boot </groupId>
  30. <artifactId>spring-boot-starter-jdbc </artifactId>
  31. </dependency>
  32. <!--lombok-->
  33. <dependency>
  34. <groupId>org.projectlombok </groupId>
  35. <artifactId>lombok </artifactId>
  36. </dependency>
  37. <!-- shiro-->
  38. <dependency>
  39. <groupId>org.apache.shiro </groupId>
  40. <artifactId>shiro-spring </artifactId>
  41. <version>1.3.2 </version>
  42. </dependency>
  43. <!--mysql-connector-->
  44. <dependency>
  45. <groupId>mysql </groupId>
  46. <artifactId>mysql-connector-java </artifactId>
  47. <scope>runtime </scope>
  48. </dependency>
  49. <!-- druid-spring-boot-starter -->
  50. <dependency>
  51. <groupId>com.alibaba </groupId>
  52. <artifactId>druid-spring-boot-starter </artifactId>
  53. <version>1.1.10 </version>
  54. </dependency>
  55. <!-- swagger -->
  56. <dependency>
  57. <groupId>com.spring4all </groupId>
  58. <artifactId>swagger-spring-boot-starter </artifactId>
  59. <version>1.8.0.RELEASE </version>
  60. </dependency>
  61. <!-- knife4j -->
  62. <dependency>
  63. <groupId>com.github.xiaoymin </groupId>
  64. <artifactId>knife4j-spring-boot-starter </artifactId>
  65. <version>2.0.2 </version>
  66. </dependency>
  67. <!-- commons-lang -->
  68. <dependency>
  69. <groupId>commons-lang </groupId>
  70. <artifactId>commons-lang </artifactId>
  71. <version>2.6 </version>
  72. </dependency>

第二步:准备好要用的包包和类类

第三步:编写登陆入口

为了方便这里不做密码加盐加密


  
  1. /**
  2. * @Author 大誌
  3. * @Date 2019/3/30 22:04
  4. * @Version 1.0
  5. */
  6. @RestController
  7. public class ShiroController {
  8. private final ShiroService shiroService;
  9. public ShiroController(ShiroService shiroService) {
  10. this.shiroService = shiroService;
  11. }
  12. /**
  13. * 登录
  14. */
  15. @ApiOperation(value = "登陆", notes = "参数:用户名 密码")
  16. @PostMapping("/sys/login")
  17. public Map<String, Object> login(@RequestBody @Validated LoginDTO loginDTO, BindingResult bindingResult) {
  18. Map<String, Object> result = new HashMap<>();
  19. if (bindingResult.hasErrors()) {
  20. result.put( "status", 400);
  21. result.put( "msg", bindingResult.getFieldError().getDefaultMessage());
  22. return result;
  23. }
  24. String username = loginDTO.getUsername();
  25. String password = loginDTO.getPassword();
  26. //用户信息
  27. User user = shiroService.findByUsername(username);
  28. //账号不存在、密码错误
  29. if (user == null || !user.getPassword().equals(password)) {
  30. result.put( "status", 400);
  31. result.put( "msg", "账号或密码有误");
  32. } else {
  33. //生成token,并保存到数据库
  34. result = shiroService.createToken(user.getUserId());
  35. result.put( "status", 200);
  36. result.put( "msg", "登陆成功");
  37. }
  38. return result;
  39. }
  40. /**
  41. * 退出
  42. */
  43. @ApiOperation(value = "登出", notes = "参数:token")
  44. @PostMapping("/sys/logout")
  45. public Map<String, Object> logout(@RequestHeader("token")String token) {
  46. Map<String, Object> result = new HashMap<>();
  47. shiroService.logout(token);
  48. result.put( "status", 200);
  49. result.put( "msg", "您已安全退出系统");
  50. return result;
  51. }
  52. }

第四步:编写ShiroService中的方法

主要是生成一个token返回给前端。


  
  1. /**
  2. * @Author 大誌
  3. * @Date 2019/3/30 22:18
  4. * @Version 1.0
  5. */
  6. @Service
  7. public class ShiroServiceImpl implements ShiroService {
  8. @Autowired
  9. private UserRepository userRepository;
  10. @Autowired
  11. private SysTokenRepository sysTokenRepository;
  12. /**
  13. * 根据username查找用户
  14. *
  15. * @param username
  16. * @return User
  17. */
  18. @Override
  19. public User findByUsername(String username) {
  20. User user = userRepository.findByUsername(username);
  21. return user;
  22. }
  23. //12小时后过期
  24. private final static int EXPIRE = 3600 * 12;
  25. @Override
  26. /**
  27. * 生成一个token
  28. *@param [userId]
  29. *@return Result
  30. */
  31. public Map<String, Object> createToken(Integer userId) {
  32. Map<String, Object> result = new HashMap<>();
  33. //生成一个token
  34. String token = TokenGenerator.generateValue();
  35. //当前时间
  36. Date now = new Date();
  37. //过期时间
  38. Date expireTime = new Date(now.getTime() + EXPIRE * 1000);
  39. //判断是否生成过token
  40. SysToken tokenEntity = sysTokenRepository.findByUserId(userId);
  41. if (tokenEntity == null) {
  42. tokenEntity = new SysToken();
  43. tokenEntity.setUserId(userId);
  44. tokenEntity.setToken(token);
  45. tokenEntity.setUpdateTime(now);
  46. tokenEntity.setExpireTime(expireTime);
  47. //保存token
  48. sysTokenRepository.save(tokenEntity);
  49. } else {
  50. tokenEntity.setToken(token);
  51. tokenEntity.setUpdateTime(now);
  52. tokenEntity.setExpireTime(expireTime);
  53. //更新token
  54. sysTokenRepository.save(tokenEntity);
  55. }
  56. result.put( "token", token);
  57. result.put( "expire", EXPIRE);
  58. return result;
  59. }
  60. @Override
  61. public void logout(String token) {
  62. SysToken byToken = findByToken(token);
  63. //生成一个token
  64. token = TokenGenerator.generateValue();
  65. //修改token
  66. SysToken tokenEntity = new SysToken();
  67. tokenEntity.setUserId(byToken.getUserId());
  68. tokenEntity.setToken(token);
  69. sysTokenRepository.save(tokenEntity);
  70. }
  71. @Override
  72. public SysToken findByToken(String accessToken) {
  73. return sysTokenRepository.findByToken(accessToken);
  74. }
  75. @Override
  76. public User findByUserId(Integer userId) {
  77. return userRepository.findByUserId(userId);
  78. }
  79. }

第五步:编写ShiroConfig类


  
  1. /**
  2. * @Author 大誌
  3. * @Date 2019/3/30 21:50
  4. * @Version 1.0
  5. */
  6. @Configuration
  7. public class ShiroConfig {
  8. @Bean("securityManager")
  9. public SecurityManager securityManager(AuthRealm authRealm) {
  10. DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
  11. securityManager.setRealm(authRealm);
  12. securityManager.setRememberMeManager( null);
  13. return securityManager;
  14. }
  15. @Bean("shiroFilter")
  16. public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
  17. ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
  18. shiroFilter.setSecurityManager(securityManager);
  19. //oauth过滤
  20. Map<String, Filter> filters = new HashMap<>();
  21. filters.put( "auth", new AuthFilter());
  22. shiroFilter.setFilters(filters);
  23. Map<String, String> filterMap = new LinkedHashMap<>();
  24. filterMap.put( "/webjars/**", "anon");
  25. filterMap.put( "/druid/**", "anon");
  26. filterMap.put( "/sys/login", "anon");
  27. filterMap.put( "/swagger/**", "anon");
  28. filterMap.put( "/v2/api-docs", "anon");
  29. filterMap.put( "/swagger-ui.html", "anon");
  30. filterMap.put( "/swagger-resources/**", "anon");
  31. filterMap.put( "/**", "auth");
  32. shiroFilter.setFilterChainDefinitionMap(filterMap);
  33. return shiroFilter;
  34. }
  35. @Bean("lifecycleBeanPostProcessor")
  36. public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
  37. return new LifecycleBeanPostProcessor();
  38. }
  39. @Bean
  40. public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
  41. AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
  42. advisor.setSecurityManager(securityManager);
  43. return advisor;
  44. }
  45. }

第六步:实现自定义的AuthenticationToken。

阅读AuthenticatingFilter抽象类中executeLogin方法,我们发现调用 了subject.login(token),这是shiro的登录方法,且需要token参数,我们自定义 AuthToken类,只要实现AuthenticationToken接口,就可以了。


  
  1. //AuthenticatingFilter中的executeLogin()
  2. protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
  3. AuthenticationToken token = createToken(request, response);
  4. if (token == null) {
  5. String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
  6. "must be created in order to execute a login attempt.";
  7. throw new IllegalStateException(msg);
  8. }
  9. try {
  10. Subject subject = getSubject(request, response);
  11. //重点!
  12. subject.login(token);
  13. return onLoginSuccess(token, subject, request, response);
  14. } catch (AuthenticationException e) {
  15. return onLoginFailure(token, e, request, response);
  16. }
  17. }

  
  1. /**
  2. * 自定义AuthenticationToken类
  3. * @Author 大誌
  4. * @Date 2019/3/31 10:58
  5. * @Version 1.0
  6. */
  7. public class AuthToken extends UsernamePasswordToken{
  8. private String token;
  9. public AuthToken(String token) {
  10. this.token = token;
  11. }
  12. @Override
  13. public Object getPrincipal() {
  14. return token;
  15. }
  16. @Override
  17. public Object getCredentials() {
  18. return token;
  19. }
  20. }

这里我实现的时候出现了Token不匹配的Bug。DeBug下可以查到源头是代码是用UsernamePasswordToken.class和我自定义的AuthToken.class配对。按道理应该是true,却返回了false...于是我就把自定义的AuthToken不实现AuthenticationToken,转为继承UsernamePasswordToken,就可以了。(renren-fast中却可以,可能是版本的问题)

2020/4/27修改: 为了避免误导,将上诉代码 AuthenticationToken 修改为 UsernamePasswordToken,并且走了一下源码,发现这个getAuthenticationTokenClass()实际上获取到的是UsernamePasswordToken.class

再回头看看renren-fast中的源码,原来他重写了supports方法!

第七步:编写自己的Realm

发起请求时,接受传过来的token后,如何保证token有效及用户权限呢?调用接口时,接受传过来的token后,如何保证token有效及用户权限呢?其实,Shiro提供了AuthorizingRealm以及AuthenticatingFilter抽象类,继承AuthorizingRealm和AuthenticatingFilter抽象类重写方法即可。


  
  1. /**
  2. * @Author 大誌
  3. * @Date 2019/3/30 21:38
  4. * @Version 1.0
  5. */
  6. @Component
  7. public class AuthRealm extends AuthorizingRealm {
  8. @Autowired
  9. private ShiroService shiroService;
  10. @Override
  11. /**
  12. * 授权 获取用户的角色和权限
  13. *@param [principals]
  14. *@return org.apache.shiro.authz.AuthorizationInfo
  15. */
  16. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  17. //1. 从 PrincipalCollection 中来获取登录用户的信息
  18. User user = (User) principals.getPrimaryPrincipal();
  19. //Integer userId = user.getUserId();
  20. //2.添加角色和权限
  21. SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
  22. for (Role role : user.getRoles()) {
  23. //2.1添加角色
  24. simpleAuthorizationInfo.addRole(role.getRoleName());
  25. for (Permission permission : role.getPermissions()) {
  26. //2.1.1添加权限
  27. simpleAuthorizationInfo.addStringPermission(permission.getPermission());
  28. }
  29. }
  30. return simpleAuthorizationInfo;
  31. }
  32. @Override
  33. /**
  34. * 认证 判断token的有效性
  35. *@param [token]
  36. *@return org.apache.shiro.authc.AuthenticationInfo
  37. */
  38. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  39. //获取token,既前端传入的token
  40. String accessToken = (String) token.getPrincipal();
  41. //1. 根据accessToken,查询用户信息
  42. SysToken tokenEntity = shiroService.findByToken(accessToken);
  43. //2. token失效
  44. if (tokenEntity == null || tokenEntity.getExpireTime().getTime() < System.currentTimeMillis()) {
  45. throw new IncorrectCredentialsException( "token失效,请重新登录");
  46. }
  47. //3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
  48. User user = shiroService.findByUserId(tokenEntity.getUserId());
  49. //4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
  50. if (user == null) {
  51. throw new UnknownAccountException( "用户不存在!");
  52. }
  53. //5. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
  54. SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, accessToken, this.getName());
  55. return info;
  56. }
  57. }

第八步:实现自定义AuthenticatingFilter。


  
  1. /**
  2. * Shiro自定义auth过滤器
  3. *
  4. * @Author 大誌
  5. * @Date 2019/3/31 10:38
  6. * @Version 1.0
  7. */
  8. @Component
  9. public class AuthFilter extends AuthenticatingFilter {
  10. // 定义jackson对象
  11. private static final ObjectMapper MAPPER = new ObjectMapper();
  12. /**
  13. * 生成自定义token
  14. *
  15. * @param request
  16. * @param response
  17. * @return
  18. * @throws Exception
  19. */
  20. @Override
  21. protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
  22. //获取请求token
  23. String token = TokenUtil.getRequestToken((HttpServletRequest) request);
  24. return new AuthToken(token);
  25. }
  26. /**
  27. * 步骤1.所有请求全部拒绝访问
  28. *
  29. * @param request
  30. * @param response
  31. * @param mappedValue
  32. * @return
  33. */
  34. @Override
  35. protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
  36. if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
  37. return true;
  38. }
  39. return false;
  40. }
  41. /**
  42. * 步骤2,拒绝访问的请求,会调用onAccessDenied方法,onAccessDenied方法先获取 token,再调用executeLogin方法
  43. *
  44. * @param request
  45. * @param response
  46. * @return
  47. * @throws Exception
  48. */
  49. @Override
  50. protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
  51. //获取请求token,如果token不存在,直接返回
  52. String token = TokenUtil.getRequestToken((HttpServletRequest) request);
  53. if (StringUtils.isBlank(token)) {
  54. HttpServletResponse httpResponse = (HttpServletResponse) response;
  55. httpResponse.setHeader( "Access-Control-Allow-Credentials", "true");
  56. httpResponse.setHeader( "Access-Control-Allow-Origin", HttpContextUtil.getOrigin());
  57. httpResponse.setCharacterEncoding( "UTF-8");
  58. Map<String, Object> result = new HashMap<>();
  59. result.put( "status", 400);
  60. result.put( "msg", "请先登录");
  61. String json = MAPPER.writeValueAsString(result);
  62. httpResponse.getWriter().print(json);
  63. return false;
  64. }
  65. return executeLogin(request, response);
  66. }
  67. /**
  68. * token失效时候调用
  69. */
  70. @Override
  71. protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
  72. HttpServletResponse httpResponse = (HttpServletResponse) response;
  73. httpResponse.setContentType( "application/json;charset=utf-8");
  74. httpResponse.setHeader( "Access-Control-Allow-Credentials", "true");
  75. httpResponse.setHeader( "Access-Control-Allow-Origin", HttpContextUtil.getOrigin());
  76. httpResponse.setCharacterEncoding( "UTF-8");
  77. try {
  78. //处理登录失败的异常
  79. Throwable throwable = e.getCause() == null ? e : e.getCause();
  80. Map<String, Object> result = new HashMap<>();
  81. result.put( "status", 400);
  82. result.put( "msg", "登录凭证已失效,请重新登录");
  83. String json = MAPPER.writeValueAsString(result);
  84. httpResponse.getWriter().print(json);
  85. } catch (IOException e1) {
  86. }
  87. return false;
  88. }
  89. }

第九步:详解校验流程

先给你们上一个超级详细的流程图。

 

接着我们打上断点按照代码走走,可能会有点啰嗦。

1. 前端发起请求首先会进入AuthFilter的 isAccessAllowed(),除了OPTION方法,其余都拦截。

2. 拦截之后进入AuthFilter的onAccessDenied(),这里获取token后判断token是否isBlank。如果是,代表请求未携带token,直接默认返回400,未登录给前端,流程就结束了。如果携带了token则进入第三步,继续流程。

3. 接着进入AuthFilter的createToken,这里生成我们自定义的AuthToken对象。

4. 接着就会来到AuthRealm中的doGetAuthenticationInfo(),在这个方法中继续token的有效性校验,例如过期、和数据库的token对不上(用户已退出)的情况。如果校验失败,进入第5步,否则进入第6步。

5. token失效后回到AuthFilter中的onLoginFailure(),返回400以及msg,流程结束。

6. Token校验成功后进入AuthRealm的doGetAuthorizationInfo(),进行获取当前用户拥有的权限,之后底层代码会进行权限验证。如果用户有权限则会进入请求方法,否则抛出异常。到这一步校验过程就结束了。

看看效果

终于熬完上面的步骤了,这时候总体的架构已经确立好了,下面让我们来看看效果如何

DTO


  
  1. /**
  2. * 登录传输类
  3. */
  4. @Data
  5. public class LoginDTO {
  6. @NotBlank(message = "用户名不能为空")
  7. private String username;
  8. @NotBlank(message = "密码不能为空")
  9. private String password;
  10. }

实体类


  
  1. @Getter
  2. @Setter
  3. @Entity
  4. public class User {
  5. @Id
  6. private Integer userId;
  7. private String username;
  8. private String password;
  9. @ManyToMany(fetch = FetchType.EAGER)
  10. @JoinTable(name = "user_role",
  11. joinColumns = {@JoinColumn(name = "USER_ID", referencedColumnName = "userId")},
  12. inverseJoinColumns = {@JoinColumn(name = "ROLE_ID", referencedColumnName = "roleId")})
  13. private Set<Role> roles;
  14. }
  15. @Getter
  16. @Setter
  17. @Entity
  18. public class Role {
  19. @Id
  20. private Integer roleId;
  21. private String roleName;
  22. @ManyToMany(fetch = FetchType.EAGER)
  23. @JoinTable(name = "role_permission",
  24. joinColumns = {@JoinColumn(name = "ROLE_ID", referencedColumnName = "roleId")},
  25. inverseJoinColumns = {@JoinColumn(name = "PERMISSION_ID", referencedColumnName = "permissionId")})
  26. private Set<Permission> permissions;
  27. }
  28. @Getter
  29. @Setter
  30. @Entity
  31. public class Permission {
  32. @Id
  33. private Integer permissionId;
  34. private String permissionName;
  35. private String permission;
  36. }
  37. @Getter
  38. @Setter
  39. @Entity
  40. public class SysToken{
  41. @Id
  42. private Integer userId;
  43. private String token;
  44. private Date expireTime;
  45. private Date updateTime
  46. }

以及给实体类附上权限:

我定义了三个用户 

用户 角色 权限
Jack SVIP select;save;delete;update
Rose VIP select;save;update
Paul P select

  
  1. /*
  2. Navicat MySQL Data Transfer
  3. Source Server : localhost
  4. Source Server Version : 50549
  5. Source Host : localhost:3306
  6. Source Database : shiro
  7. Target Server Type : MYSQL
  8. Target Server Version : 50549
  9. File Encoding : 65001
  10. Date: 2019-04-07 17:06:36
  11. */
  12. SET FOREIGN_KEY_CHECKS= 0;
  13. -- ----------------------------
  14. -- Table structure for permission
  15. -- ----------------------------
  16. DROP TABLE IF EXISTS `permission`;
  17. CREATE TABLE `permission` (
  18. `permission_id` int( 11) NOT NULL,
  19. `permission` varchar( 255) DEFAULT NULL,
  20. `permission_name` varchar( 255) DEFAULT NULL,
  21. PRIMARY KEY ( `permission_id`)
  22. ) ENGINE= InnoDB DEFAULT CHARSET=utf8;
  23. -- ----------------------------
  24. -- Records of permission
  25. -- ----------------------------
  26. INSERT INTO `permission` VALUES ( '1', 'select', '查看');
  27. INSERT INTO `permission` VALUES ( '2', 'update', '更新');
  28. INSERT INTO `permission` VALUES ( '3', 'delete', '删除');
  29. INSERT INTO `permission` VALUES ( '4', 'save', '新增');
  30. -- ----------------------------
  31. -- Table structure for role
  32. -- ----------------------------
  33. DROP TABLE IF EXISTS `role`;
  34. CREATE TABLE `role` (
  35. `role_id` int( 11) NOT NULL,
  36. `role_name` varchar( 255) DEFAULT NULL,
  37. PRIMARY KEY ( `role_id`)
  38. ) ENGINE= InnoDB DEFAULT CHARSET=utf8;
  39. -- ----------------------------
  40. -- Records of role
  41. -- ----------------------------
  42. INSERT INTO `role` VALUES ( '1', 'svip');
  43. INSERT INTO `role` VALUES ( '2', 'vip');
  44. INSERT INTO `role` VALUES ( '3', 'p');
  45. -- ----------------------------
  46. -- Table structure for role_permission
  47. -- ----------------------------
  48. DROP TABLE IF EXISTS `role_permission`;
  49. CREATE TABLE `role_permission` (
  50. `role_id` int( 11) NOT NULL,
  51. `permission_id` int( 11) NOT NULL,
  52. PRIMARY KEY ( `role_id`, `permission_id`),
  53. KEY `FKf8yllw1ecvwqy3ehyxawqa1qp` ( `permission_id`),
  54. CONSTRAINT `FKa6jx8n8xkesmjmv6jqug6bg68` FOREIGN KEY ( `role_id`) REFERENCES `role` ( `role_id`),
  55. CONSTRAINT `FKf8yllw1ecvwqy3ehyxawqa1qp` FOREIGN KEY ( `permission_id`) REFERENCES `permission` ( `permission_id`)
  56. ) ENGINE= InnoDB DEFAULT CHARSET=utf8;
  57. -- ----------------------------
  58. -- Records of role_permission
  59. -- ----------------------------
  60. INSERT INTO `role_permission` VALUES ( '1', '1');
  61. INSERT INTO `role_permission` VALUES ( '2', '1');
  62. INSERT INTO `role_permission` VALUES ( '3', '1');
  63. INSERT INTO `role_permission` VALUES ( '1', '2');
  64. INSERT INTO `role_permission` VALUES ( '2', '2');
  65. INSERT INTO `role_permission` VALUES ( '1', '3');
  66. INSERT INTO `role_permission` VALUES ( '1', '4');
  67. INSERT INTO `role_permission` VALUES ( '2', '4');
  68. -- ----------------------------
  69. -- Table structure for user
  70. -- ----------------------------
  71. DROP TABLE IF EXISTS `user`;
  72. CREATE TABLE `user` (
  73. `user_id` int( 11) NOT NULL,
  74. `password` varchar( 255) DEFAULT NULL,
  75. `username` varchar( 255) DEFAULT NULL,
  76. PRIMARY KEY ( `user_id`)
  77. ) ENGINE= InnoDB DEFAULT CHARSET=utf8;
  78. -- ----------------------------
  79. -- Records of user
  80. -- ----------------------------
  81. INSERT INTO `user` VALUES ( '1', '123', 'Jack');
  82. INSERT INTO `user` VALUES ( '2', '123', 'Rose');
  83. INSERT INTO `user` VALUES ( '3', '123', 'Paul');
  84. -- ----------------------------
  85. -- Table structure for user_role
  86. -- ----------------------------
  87. DROP TABLE IF EXISTS `user_role`;
  88. CREATE TABLE `user_role` (
  89. `user_id` int( 11) NOT NULL,
  90. `role_id` int( 11) NOT NULL,
  91. PRIMARY KEY ( `user_id`, `role_id`),
  92. KEY `FKa68196081fvovjhkek5m97n3y` ( `role_id`),
  93. CONSTRAINT `FK859n2jvi8ivhui0rl0esws6o` FOREIGN KEY ( `user_id`) REFERENCES `user` ( `user_id`),
  94. CONSTRAINT `FKa68196081fvovjhkek5m97n3y` FOREIGN KEY ( `role_id`) REFERENCES `role` ( `role_id`)
  95. ) ENGINE= InnoDB DEFAULT CHARSET=utf8;
  96. -- ----------------------------
  97. -- Table structure for sys_token
  98. -- ----------------------------
  99. CREATE TABLE `sys_token` (
  100. `user_id` int( 11) NOT NULL,
  101. `expire_time` datetime DEFAULT NULL,
  102. `token` varchar( 255) DEFAULT NULL,
  103. `update_time` datetime DEFAULT NULL,
  104. PRIMARY KEY ( `user_id`)
  105. ) ENGINE= InnoDB DEFAULT CHARSET=utf8;
  106. -- ----------------------------
  107. -- Records of user_role
  108. -- ----------------------------
  109. INSERT INTO `user_role` VALUES ( '1', '1');
  110. INSERT INTO `user_role` VALUES ( '2', '2');
  111. INSERT INTO `user_role` VALUES ( '3', '3');

测试类:因为我是用Swagger来测试,所以为了方便就直接传递token参数。具体开发时候可由前端把接收到的token放入Header。


  
  1. /**
  2. * @Author 大誌
  3. * @Date 2019/4/7 15:20
  4. * @Version 1.0
  5. */
  6. @RestController("/test")
  7. public class TestController {
  8. @RequiresPermissions({"save"}) //没有的话 AuthorizationException
  9. @PostMapping("/save")
  10. public Map<String, Object> save(String token) {
  11. System.out.println( "save");
  12. Map<String, Object> map = new HashMap<String, Object>();
  13. map.put( "status", 200);
  14. map.put( "msg", "当前用户有save的权力");
  15. return map;
  16. }
  17. @RequiresPermissions({"delete"}) //没有的话 AuthorizationException
  18. @DeleteMapping("/delete")
  19. public Map<String, Object> delete(String token) {
  20. System.out.println( "delete");
  21. Map<String, Object> map = new HashMap<String, Object>();
  22. map.put( "status", 200);
  23. map.put( "msg", "当前用户有delete的权力");
  24. return map;
  25. }
  26. @RequiresPermissions({"update"}) //没有的话 AuthorizationException
  27. @PutMapping("update")
  28. public Map<String, Object> update(String token) {
  29. System.out.println( "update");
  30. Map<String, Object> map = new HashMap<String, Object>();
  31. map.put( "status", 200);
  32. map.put( "msg", "当前用户有update的权力");
  33. return map;
  34. }
  35. @RequiresPermissions({"select"}) //没有的话 AuthorizationException
  36. @GetMapping("select")
  37. public Map<String, Object> select(String token, HttpSession session) {
  38. System.out.println( "select");
  39. Map<String, Object> map = new HashMap<String, Object>();
  40. map.put( "status", 200);
  41. map.put( "msg", "当前用户有select的权力");
  42. return map;
  43. }
  44. @RequiresRoles({"vip"}) //没有的话 AuthorizationException
  45. @GetMapping("/vip")
  46. public Map<String, Object> vip(String token) {
  47. System.out.println( "vip");
  48. Map<String, Object> map = new HashMap<String, Object>();
  49. map.put( "status", 200);
  50. map.put( "msg", "当前用户有VIP角色");
  51. return map;
  52. }
  53. @RequiresRoles({"svip"}) //没有的话 AuthorizationException
  54. @GetMapping("/svip")
  55. public Map<String, Object> svip(String token) {
  56. System.out.println( "svip");
  57. Map<String, Object> map = new HashMap<String, Object>();
  58. map.put( "status", 200);
  59. map.put( "msg", "当前用户有SVIP角色");
  60. return map;
  61. }
  62. @RequiresRoles({"p"}) //没有的话 AuthorizationException
  63. @GetMapping("/p")
  64. public Map<String, Object> p(String token) {
  65. System.out.println( "p");
  66. Map<String, Object> map = new HashMap<String, Object>();
  67. map.put( "status", 200);
  68. map.put( "msg", "当前用户有P角色");
  69. return map;
  70. }
  71. }

ExceptionHandler 异常处理器,用于捕获无权限时候的异常。


  
  1. @ControllerAdvice
  2. public class MyExceptionHandler {
  3. @ExceptionHandler(value = AuthorizationException.class)
  4. @ResponseBody
  5. public Map<String, String> handleException(AuthorizationException e) {
  6. //e.printStackTrace();
  7. Map<String, String> result = new HashMap<String, String>();
  8. result.put( "status", "400");
  9. //获取错误中中括号的内容
  10. String message = e.getMessage();
  11. String msg=message.substring(message.indexOf( "[")+ 1,message.indexOf( "]"));
  12. //判断是角色错误还是权限错误
  13. if (message.contains( "role")) {
  14. result.put( "msg", "对不起,您没有" + msg + "角色");
  15. } else if (message.contains( "permission")) {
  16. result.put( "msg", "对不起,您没有" + msg + "权限");
  17. } else {
  18. result.put( "msg", "对不起,您的权限有误");
  19. }
  20. return result;
  21. }
  22. }

启动项目来看看效果: 访问 localhost:9090/shiro/doc.html

登陆失败:

登陆成功:

登录成功后会返回token,记得带上token访问以下接口

有某个角色时候:

没有某个角色的时候:

有某个权力时候:

没有某个权力的时候:

退出系统

原本的token就失效了,我们再访问原本可以访问的接口看看

至此就已经进入尾声了

2020/3/27 新编写了VUE+Element前端页面

正常访问:

非法访问:

重点:当未登录时候访问项目内部页面,由前端控制路由返回登录页,并不会出现可恶的login.jsp,这里我们故意改变数据库token来展示效果。

总结

至于最后没有权利或角色返回的json字符串是因为他抛出AuthorizationException。可以自定义全局异常处理器进行处理。通过这种token达到即可达到前后端分离开发。各位客官,点个赞吧qaq。

2019/11/26日修改:在后续开发中,发现shiro如果使用ShiroConfig中shiroFiltet的map进行权限或角色拦截,会出现只走登陆认证,不走授权认证的情况。这是个巨坑!后续再写一篇文章深究一下。解决方法:使用注解@RequiresRoles() 以及@RequiresPermissions()进行权限和角色拦截


  
  1. @Bean("shiroFilter")
  2. public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
  3. ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
  4. shiroFilter.setSecurityManager(securityManager);
  5. //自定义过滤(关键)
  6. Map<String, Filter> filters = new HashMap<>();
  7. filters.put( "auth", new AuthFilter());
  8. shiroFilter.setFilters(filters);
  9. Map<String, String> filterMap = new LinkedHashMap<>();
  10. //主要是这部分: 不要用这种方法,最好用注解的方法
  11. filterMap.put( "/add", "roles[admin]");
  12. filterMap.put( "/list", "roles[admin,user]");
  13. filterMap.put( "/delete", "perms[admin:delete]");
  14. filterMap.put( "/**", "auth");
  15. shiroFilter.setFilterChainDefinitionMap(filterMap);
  16. return shiroFilter;
  17. }

2020/3/25补充,修改了部分不符合规范的代码,添加了全局异常捕获器。同时补充了校验流程。同时提示两句,因为token频繁在客户端和服务器端传输,因此可能会造成token劫持攻击(既黑客捕获你的token之后就可以代替你为所欲为),如果对这方面有安全隐患的担忧,可以采取每访问一次接口,更新一次token。并且我这里处于方便的原因是采用了mysql存储token,具体开发中应该用redis缓存来存储。


有什么问题可以评论或者私信我,每日在线解(LIAO)疑(SAO)。

我是大誌,一位准备996的卑微码农🐶,觉得好用记得点赞收藏!!!


转载:https://blog.csdn.net/weixin_42236404/article/details/89319359
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场