前言
前后端分离,一般都是通过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 {
@Autowired
private UserRepository userRepository;
@Autowired
private SysTokenRepository sysTokenRepository;
/**
* 根据username查找用户
*
* @param username
* @return User
*/
@Override
public 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<>();
//生成一个token
String token = TokenGenerator.generateValue();
//当前时间
Date now = new Date();
//过期时间
Date expireTime = new Date(now.getTime() + EXPIRE * 1000);
//判断是否生成过token
SysToken tokenEntity = sysTokenRepository.findByUserId(userId);
if (tokenEntity == null) {
tokenEntity = new SysToken();
tokenEntity.setUserId(userId);
tokenEntity.setToken(token);
tokenEntity.setUpdateTime(now);
tokenEntity.setExpireTime(expireTime);
//保存token
sysTokenRepository.save(tokenEntity);
} else {
tokenEntity.setToken(token);
tokenEntity.setUpdateTime(now);
tokenEntity.setExpireTime(expireTime);
//更新token
sysTokenRepository.save(tokenEntity);
}
result.put("token", token);
result.put("expire", EXPIRE);
return result;
}
@Override
public void logout(String token) {
SysToken byToken = findByToken(token);
//生成一个token
token = TokenGenerator.generateValue();
//修改token
SysToken tokenEntity = new SysToken();
tokenEntity.setUserId(byToken.getUserId());
tokenEntity.setToken(token);
sysTokenRepository.save(tokenEntity);
}
@Override
public SysToken findByToken(String accessToken) {
return sysTokenRepository.findByToken(accessToken);
}
@Override
public 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();
}
@Bean
public 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;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public 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 {
@Autowired
private 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,既前端传入的token
String 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 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
SimpleAuthenticationInfo 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
*/
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
//获取请求token
String token = TokenUtil.getRequestToken((HttpServletRequest) request);
return new AuthToken(token);
}
/**
* 步骤1.所有请求全部拒绝访问
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected 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
*/
@Override
protected 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失效时候调用
*/
@Override
protected 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 {
@Id
private 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 {
@Id
private 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 {
@Id
private Integer permissionId;
private String permissionName;
private String permission;
}
@Getter
@Setter
@Entity
public class SysToken{
@Id
private 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)
@ResponseBody
public 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缓存来存储。