前言
前后端分离,一般都是通过token实现,本项目也是一样;用户登录时,生成token及 token过期时间,token与用户是一一对应关系,调用接口的时候,把token放到header或 请求参数中,服务端就知道是谁在调用接口。
代码已上传到Git:
后台代码:https://github.com/FENGZHIJIE1998/shiro-auth
前端代码:https://github.com/FENGZHIJIE1998/shiro-vue
觉得好用的记得点个Star哦
原文链接:https://blog.csdn.net/weixin_42236404/article/details/89319359
第一步:新建工程
第二步:准备好要用的包包和类类
第三步:编写登陆入口
第四步:编写ShiroService中的方法
第五步:编写ShiroConfig类
第六步:实现自定义的AuthenticationToken。
第七步:编写自己的Realm
第八步:实现自定义AuthenticatingFilter。
第九步:详解校验流程
看看效果
第一步:新建工程
pom文件application.yml巴拉巴拉这里省略,这里贴出需要用到的依赖:
<!--starter--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><!-- test--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!--web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--validation--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><!--JPA--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><!--JDBC--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><!--lombok--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!-- shiro--><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.3.2</version></dependency><!--mysql-connector--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!-- druid-spring-boot-starter --><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.10</version></dependency><!-- swagger --><dependency><groupId>com.spring4all</groupId><artifactId>swagger-spring-boot-starter</artifactId><version>1.8.0.RELEASE</version></dependency><!-- knife4j --><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><version>2.0.2</version></dependency><!-- commons-lang --><dependency><groupId>commons-lang</groupId><artifactId>commons-lang</artifactId><version>2.6</version></dependency>
第二步:准备好要用的包包和类类

第三步:编写登陆入口
为了方便这里不做密码加盐加密:
/*** @Author 大誌* @Date 2019/3/30 22:04* @Version 1.0*/
@RestController
public class ShiroController {private final ShiroService shiroService;public ShiroController(ShiroService shiroService) {this.shiroService = shiroService;}/*** 登录*/@ApiOperation(value = "登陆", notes = "参数:用户名 密码")@PostMapping("/sys/login")public Map<String, Object> login(@RequestBody @Validated LoginDTO loginDTO, BindingResult bindingResult) {Map<String, Object> result = new HashMap<>();if (bindingResult.hasErrors()) {result.put("status", 400);result.put("msg", bindingResult.getFieldError().getDefaultMessage());return result;}String username = loginDTO.getUsername();String password = loginDTO.getPassword();//用户信息User user = shiroService.findByUsername(username);//账号不存在、密码错误if (user == null || !user.getPassword().equals(password)) {result.put("status", 400);result.put("msg", "账号或密码有误");} else {//生成token,并保存到数据库result = shiroService.createToken(user.getUserId());result.put("status", 200);result.put("msg", "登陆成功");}return result;}/*** 退出*/@ApiOperation(value = "登出", notes = "参数:token")@PostMapping("/sys/logout")public Map<String, Object> logout(@RequestHeader("token")String token) {Map<String, Object> result = new HashMap<>();shiroService.logout(token);result.put("status", 200);result.put("msg", "您已安全退出系统");return result;}
}
第四步:编写ShiroService中的方法
主要是生成一个token返回给前端。
/*** @Author 大誌* @Date 2019/3/30 22:18* @Version 1.0*/
@Service
public class ShiroServiceImpl implements ShiroService {@Autowiredprivate UserRepository userRepository;@Autowiredprivate SysTokenRepository sysTokenRepository;/*** 根据username查找用户** @param username* @return User*/@Overridepublic User findByUsername(String username) {User user = userRepository.findByUsername(username);return user;}//12小时后过期private final static int EXPIRE = 3600 * 12;@Override/*** 生成一个token*@param [userId]*@return Result*/public Map<String, Object> createToken(Integer userId) {Map<String, Object> result = new HashMap<>();//生成一个tokenString token = TokenGenerator.generateValue();//当前时间Date now = new Date();//过期时间Date expireTime = new Date(now.getTime() + EXPIRE * 1000);//判断是否生成过tokenSysToken tokenEntity = sysTokenRepository.findByUserId(userId);if (tokenEntity == null) {tokenEntity = new SysToken();tokenEntity.setUserId(userId);tokenEntity.setToken(token);tokenEntity.setUpdateTime(now);tokenEntity.setExpireTime(expireTime);//保存tokensysTokenRepository.save(tokenEntity);} else {tokenEntity.setToken(token);tokenEntity.setUpdateTime(now);tokenEntity.setExpireTime(expireTime);//更新tokensysTokenRepository.save(tokenEntity);}result.put("token", token);result.put("expire", EXPIRE);return result;}@Overridepublic void logout(String token) {SysToken byToken = findByToken(token);//生成一个tokentoken = TokenGenerator.generateValue();//修改tokenSysToken tokenEntity = new SysToken();tokenEntity.setUserId(byToken.getUserId());tokenEntity.setToken(token);sysTokenRepository.save(tokenEntity);}@Overridepublic SysToken findByToken(String accessToken) {return sysTokenRepository.findByToken(accessToken);}@Overridepublic User findByUserId(Integer userId) {return userRepository.findByUserId(userId);}
}
第五步:编写ShiroConfig类
/*** @Author 大誌* @Date 2019/3/30 21:50* @Version 1.0*/
@Configuration
public class ShiroConfig {@Bean("securityManager")public SecurityManager securityManager(AuthRealm authRealm) {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();securityManager.setRealm(authRealm);securityManager.setRememberMeManager(null);return securityManager;}@Bean("shiroFilter")public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();shiroFilter.setSecurityManager(securityManager);//oauth过滤Map<String, Filter> filters = new HashMap<>();filters.put("auth", new AuthFilter());shiroFilter.setFilters(filters);Map<String, String> filterMap = new LinkedHashMap<>();filterMap.put("/webjars/**", "anon");filterMap.put("/druid/**", "anon");filterMap.put("/sys/login", "anon");filterMap.put("/swagger/**", "anon");filterMap.put("/v2/api-docs", "anon");filterMap.put("/swagger-ui.html", "anon");filterMap.put("/swagger-resources/**", "anon");filterMap.put("/**", "auth");shiroFilter.setFilterChainDefinitionMap(filterMap);return shiroFilter;}@Bean("lifecycleBeanPostProcessor")public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {return new LifecycleBeanPostProcessor();}@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();advisor.setSecurityManager(securityManager);return advisor;}
}
第六步:实现自定义的AuthenticationToken。
阅读AuthenticatingFilter抽象类中executeLogin方法,我们发现调用 了subject.login(token),这是shiro的登录方法,且需要token参数,我们自定义 AuthToken类,只要实现AuthenticationToken接口,就可以了。
//AuthenticatingFilter中的executeLogin()
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {AuthenticationToken token = createToken(request, response);if (token == null) {String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +"must be created in order to execute a login attempt.";throw new IllegalStateException(msg);}try {Subject subject = getSubject(request, response);//重点!subject.login(token);return onLoginSuccess(token, subject, request, response);} catch (AuthenticationException e) {return onLoginFailure(token, e, request, response);}}
/*** 自定义AuthenticationToken类* @Author 大誌* @Date 2019/3/31 10:58* @Version 1.0*/
public class AuthToken extends UsernamePasswordToken{private String token;public AuthToken(String token) {this.token = token;}@Overridepublic Object getPrincipal() {return token;}@Overridepublic Object getCredentials() {return token;}
}
这里我实现的时候出现了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抽象类重写方法即可
/*** @Author 大誌* @Date 2019/3/30 21:38* @Version 1.0*/
@Component
public class AuthRealm extends AuthorizingRealm {@Autowiredprivate ShiroService shiroService;@Override/*** 授权 获取用户的角色和权限*@param [principals]*@return org.apache.shiro.authz.AuthorizationInfo*/protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {//1. 从 PrincipalCollection 中来获取登录用户的信息User user = (User) principals.getPrimaryPrincipal();//Integer userId = user.getUserId();//2.添加角色和权限SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();for (Role role : user.getRoles()) {//2.1添加角色simpleAuthorizationInfo.addRole(role.getRoleName());for (Permission permission : role.getPermissions()) {//2.1.1添加权限simpleAuthorizationInfo.addStringPermission(permission.getPermission());}}return simpleAuthorizationInfo;}@Override/*** 认证 判断token的有效性*@param [token]*@return org.apache.shiro.authc.AuthenticationInfo*/protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {//获取token,既前端传入的tokenString accessToken = (String) token.getPrincipal();//1. 根据accessToken,查询用户信息SysToken tokenEntity = shiroService.findByToken(accessToken);//2. token失效if (tokenEntity == null || tokenEntity.getExpireTime().getTime() < System.currentTimeMillis()) {throw new IncorrectCredentialsException("token失效,请重新登录");}//3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录User user = shiroService.findByUserId(tokenEntity.getUserId());//4. 若用户不存在, 则可以抛出 UnknownAccountException 异常if (user == null) {throw new UnknownAccountException("用户不存在!");}//5. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfoSimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, accessToken, this.getName());return info;}
}
第八步:实现自定义AuthenticatingFilter。
/*** Shiro自定义auth过滤器** @Author 大誌* @Date 2019/3/31 10:38* @Version 1.0*/
@Component
public class AuthFilter extends AuthenticatingFilter {// 定义jackson对象private static final ObjectMapper MAPPER = new ObjectMapper();/*** 生成自定义token** @param request* @param response* @return* @throws Exception*/@Overrideprotected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {//获取请求tokenString token = TokenUtil.getRequestToken((HttpServletRequest) request);return new AuthToken(token);}/*** 步骤1.所有请求全部拒绝访问** @param request* @param response* @param mappedValue* @return*/@Overrideprotected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {return true;}return false;}/*** 步骤2,拒绝访问的请求,会调用onAccessDenied方法,onAccessDenied方法先获取 token,再调用executeLogin方法** @param request* @param response* @return* @throws Exception*/@Overrideprotected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {//获取请求token,如果token不存在,直接返回String token = TokenUtil.getRequestToken((HttpServletRequest) request);if (StringUtils.isBlank(token)) {HttpServletResponse httpResponse = (HttpServletResponse) response;httpResponse.setHeader("Access-Control-Allow-Credentials", "true");httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtil.getOrigin());httpResponse.setCharacterEncoding("UTF-8");Map<String, Object> result = new HashMap<>();result.put("status", 400);result.put("msg", "请先登录");String json = MAPPER.writeValueAsString(result);httpResponse.getWriter().print(json);return false;}return executeLogin(request, response);}/*** token失效时候调用*/@Overrideprotected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {HttpServletResponse httpResponse = (HttpServletResponse) response;httpResponse.setContentType("application/json;charset=utf-8");httpResponse.setHeader("Access-Control-Allow-Credentials", "true");httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtil.getOrigin());httpResponse.setCharacterEncoding("UTF-8");try {//处理登录失败的异常Throwable throwable = e.getCause() == null ? e : e.getCause();Map<String, Object> result = new HashMap<>();result.put("status", 400);result.put("msg", "登录凭证已失效,请重新登录");String json = MAPPER.writeValueAsString(result);httpResponse.getWriter().print(json);} catch (IOException e1) {}return false;}}
第九步:详解校验流程
先给你们上一个超级详细的流程图。

接着我们打上断点按照代码走走,可能会有点啰嗦。
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
/*** 登录传输类*/
@Data
public class LoginDTO {@NotBlank(message = "用户名不能为空")private String username;@NotBlank(message = "密码不能为空")private String password;
}
实体类
@Getter
@Setter
@Entity
public class User {@Idprivate Integer userId;private String username;private String password;@ManyToMany(fetch = FetchType.EAGER)@JoinTable(name = "user_role",joinColumns = {@JoinColumn(name = "USER_ID", referencedColumnName = "userId")},inverseJoinColumns = {@JoinColumn(name = "ROLE_ID", referencedColumnName = "roleId")})private Set<Role> roles;}@Getter
@Setter
@Entity
public class Role {@Idprivate Integer roleId;private String roleName;@ManyToMany(fetch = FetchType.EAGER)@JoinTable(name = "role_permission",joinColumns = {@JoinColumn(name = "ROLE_ID", referencedColumnName = "roleId")},inverseJoinColumns = {@JoinColumn(name = "PERMISSION_ID", referencedColumnName = "permissionId")})private Set<Permission> permissions;
}@Getter
@Setter
@Entity
public class Permission {@Idprivate Integer permissionId;private String permissionName;private String permission;
}@Getter
@Setter
@Entity
public class SysToken{@Idprivate Integer userId;private String token;private Date expireTime;private Date updateTime
}
以及给实体类附上权限:
我定义了三个用户
用户 角色 权限
Jack SVIP select;save;delete;update
Rose VIP select;save;update
Paul P select
/*
Navicat MySQL Data Transfer
Source Server : localhost
Source Server Version : 50549
Source Host : localhost:3306
Source Database : shiro
Target Server Type : MYSQL
Target Server Version : 50549
File Encoding : 65001
Date: 2019-04-07 17:06:36
*/SET FOREIGN_KEY_CHECKS=0;-- ----------------------------
-- Table structure for permission
-- ----------------------------
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission` (`permission_id` int(11) NOT NULL,`permission` varchar(255) DEFAULT NULL,`permission_name` varchar(255) DEFAULT NULL,PRIMARY KEY (`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ----------------------------
-- Records of permission
-- ----------------------------
INSERT INTO `permission` VALUES ('1', 'select', '查看');
INSERT INTO `permission` VALUES ('2', 'update', '更新');
INSERT INTO `permission` VALUES ('3', 'delete', '删除');
INSERT INTO `permission` VALUES ('4', 'save', '新增');-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (`role_id` int(11) NOT NULL,`role_name` varchar(255) DEFAULT NULL,PRIMARY KEY (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES ('1', 'svip');
INSERT INTO `role` VALUES ('2', 'vip');
INSERT INTO `role` VALUES ('3', 'p');-- ----------------------------
-- Table structure for role_permission
-- ----------------------------
DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission` (`role_id` int(11) NOT NULL,`permission_id` int(11) NOT NULL,PRIMARY KEY (`role_id`,`permission_id`),KEY `FKf8yllw1ecvwqy3ehyxawqa1qp` (`permission_id`),CONSTRAINT `FKa6jx8n8xkesmjmv6jqug6bg68` FOREIGN KEY (`role_id`) REFERENCES `role` (`role_id`),CONSTRAINT `FKf8yllw1ecvwqy3ehyxawqa1qp` FOREIGN KEY (`permission_id`) REFERENCES `permission` (`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ----------------------------
-- Records of role_permission
-- ----------------------------
INSERT INTO `role_permission` VALUES ('1', '1');
INSERT INTO `role_permission` VALUES ('2', '1');
INSERT INTO `role_permission` VALUES ('3', '1');
INSERT INTO `role_permission` VALUES ('1', '2');
INSERT INTO `role_permission` VALUES ('2', '2');
INSERT INTO `role_permission` VALUES ('1', '3');
INSERT INTO `role_permission` VALUES ('1', '4');
INSERT INTO `role_permission` VALUES ('2', '4');-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (`user_id` int(11) NOT NULL,`password` varchar(255) DEFAULT NULL,`username` varchar(255) DEFAULT NULL,PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', '123', 'Jack');
INSERT INTO `user` VALUES ('2', '123', 'Rose');
INSERT INTO `user` VALUES ('3', '123', 'Paul');-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (`user_id` int(11) NOT NULL,`role_id` int(11) NOT NULL,PRIMARY KEY (`user_id`,`role_id`),KEY `FKa68196081fvovjhkek5m97n3y` (`role_id`),CONSTRAINT `FK859n2jvi8ivhui0rl0esws6o` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`),CONSTRAINT `FKa68196081fvovjhkek5m97n3y` FOREIGN KEY (`role_id`) REFERENCES `role` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ----------------------------
-- Table structure for sys_token
-- ----------------------------
CREATE TABLE `sys_token` (`user_id` int(11) NOT NULL,`expire_time` datetime DEFAULT NULL,`token` varchar(255) DEFAULT NULL,`update_time` datetime DEFAULT NULL,PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES ('1', '1');
INSERT INTO `user_role` VALUES ('2', '2');
INSERT INTO `user_role` VALUES ('3', '3');
测试类:因为我是用Swagger来测试,所以为了方便就直接传递token参数。具体开发时候可由前端把接收到的token放入Header。
/*** @Author 大誌* @Date 2019/4/7 15:20* @Version 1.0*/
@RestController
public class TestController {@RequiresPermissions({"save"}) //没有的话 AuthorizationException@PostMapping("/save")public Map<String, Object> save(String token) {System.out.println("save");Map<String, Object> map = new HashMap<String, Object>();map.put("status", 200);map.put("msg", "当前用户有save的权力");return map;}@RequiresPermissions({"delete"}) //没有的话 AuthorizationException@DeleteMapping("/delete")public Map<String, Object> delete(String token) {System.out.println("delete");Map<String, Object> map = new HashMap<String, Object>();map.put("status", 200);map.put("msg", "当前用户有delete的权力");return map;}@RequiresPermissions({"update"}) //没有的话 AuthorizationException@PutMapping("update")public Map<String, Object> update(String token) {System.out.println("update");Map<String, Object> map = new HashMap<String, Object>();map.put("status", 200);map.put("msg", "当前用户有update的权力");return map;}@RequiresPermissions({"select"}) //没有的话 AuthorizationException@GetMapping("select")public Map<String, Object> select(String token, HttpSession session) {System.out.println("select");Map<String, Object> map = new HashMap<String, Object>();map.put("status", 200);map.put("msg", "当前用户有select的权力");return map;}@RequiresRoles({"vip"}) //没有的话 AuthorizationException@GetMapping("/vip")public Map<String, Object> vip(String token) {System.out.println("vip");Map<String, Object> map = new HashMap<String, Object>();map.put("status", 200);map.put("msg", "当前用户有VIP角色");return map;}@RequiresRoles({"svip"}) //没有的话 AuthorizationException@GetMapping("/svip")public Map<String, Object> svip(String token) {System.out.println("svip");Map<String, Object> map = new HashMap<String, Object>();map.put("status", 200);map.put("msg", "当前用户有SVIP角色");return map;}@RequiresRoles({"p"}) //没有的话 AuthorizationException@GetMapping("/p")public Map<String, Object> p(String token) {System.out.println("p");Map<String, Object> map = new HashMap<String, Object>();map.put("status", 200);map.put("msg", "当前用户有P角色");return map;}
}
ExceptionHandler 异常处理器,用于捕获无权限时候的异常
@ControllerAdvice
public class MyExceptionHandler {@ExceptionHandler(value = AuthorizationException.class)@ResponseBodypublic Map<String, String> handleException(AuthorizationException e) {//e.printStackTrace();Map<String, String> result = new HashMap<String, String>();result.put("status", "400");//获取错误中中括号的内容String message = e.getMessage();String msg=message.substring(message.indexOf("[")+1,message.indexOf("]"));//判断是角色错误还是权限错误if (message.contains("role")) {result.put("msg", "对不起,您没有" + msg + "角色");} else if (message.contains("permission")) {result.put("msg", "对不起,您没有" + msg + "权限");} else {result.put("msg", "对不起,您的权限有误");}return result;}
}
启动项目来看看效果: 访问 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()进行权限和角色拦截
@Bean("shiroFilter")public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();shiroFilter.setSecurityManager(securityManager);//自定义过滤(关键)Map<String, Filter> filters = new HashMap<>();filters.put("auth", new AuthFilter());shiroFilter.setFilters(filters);Map<String, String> filterMap = new LinkedHashMap<>();//主要是这部分: 不要用这种方法,最好用注解的方法filterMap.put("/add", "roles[admin]");filterMap.put("/list", "roles[admin,user]");filterMap.put("/delete", "perms[admin:delete]");filterMap.put("/**", "auth");shiroFilter.setFilterChainDefinitionMap(filterMap);return shiroFilter;}
2020/3/25补充,修改了部分不符合规范的代码,添加了全局异常捕获器。同时补充了校验流程。同时提示两句,因为token频繁在客户端和服务器端传输,因此可能会造成token劫持攻击(既黑客捕获你的token之后就可以代替你为所欲为),如果对这方面有安全隐患的担忧,可以采取每访问一次接口,更新一次token。并且我这里处于方便的原因是采用了mysql存储token,具体开发中应该用redis缓存来存储。