SpringBoot请求权限控制Shiro
目录
SpringBoot请求权限控制——Shiro
Shiro是一个请求权限校验框架,本文介绍如何与SpringBoot集成使用。
登录校验
1、增加maven依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.13.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.13.0</version>
</dependency>
2、shiro配置类
package com.wzu.utilities.config.shiro;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.Filter;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.ValueOperations;
@Configuration
public class ShiroConfig {
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager, ValueOperations<String, Object> valueOperations, @Value("${shiro.session-timeout}") Long sessionTimeout) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
Map<String, Filter> filters = new HashMap<>();
filters.put("authc", new MyAuthenticationFilter(valueOperations, sessionTimeout));
shiroFilterFactoryBean.setFilters(filters);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm());
return securityManager;
}
@Bean
public CustomRealm customRealm() {
return new CustomRealm();
}
}
授权逻辑类CustomRealm
package com.wzu.utilities.config.shiro;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AccountException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.wzu.utilities.entity.po.User;
import com.wzu.utilities.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CustomRealm extends AuthorizingRealm {
@Resource
UserMapper userMapper;
@Resource
ValueOperations<String, String> valueOperations;
@Resource
RedisTemplate redisTemplate;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String token = (String) principals.getPrimaryPrincipal();
String userStr = valueOperations.get(token);
if (StringUtils.isBlank(userStr)) {
throw new AccountException("登录失效,请重新登录");
}
User user = JSONObject.parseObject(userStr, User.class);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//TODO:info.setRoles();
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String name = (String) authenticationToken.getPrincipal();
String password = new String((char[]) authenticationToken.getCredentials());
User user = userMapper.selectOne(new QueryWrapper<User>().eq("username", name));
if (user==null || !password.equals(user.getPassword())) {
throw new AuthenticationException("用户名或密码错误");
}
Session session = SecurityUtils.getSubject().getSession();
String token = (String)session.getAttribute(WebConstants.LOGIN_USER_TOKEN);
if (StringUtils.isBlank(token)) {
// for(Session activeSession:sessionDAO.getActiveSessions()) {
// if(user.getId().equals(activeSession.getAttribute(WebConstants.LOGIN_USER_ID)) && !activeSession.getId().equals(session.getId())) {
// String tmpToken = (String)activeSession.getAttribute(WebConstants.LOGIN_USER_TOKEN);
// if(StringUtils.isNotBlank(tmpToken)) {
// redisTemplate.delete(tmpToken);
// }
// sessionDAO.delete(activeSession);
// }
// }
// if (user.getLoginNum()!=1) {
// user.setLoginNum(user.getLoginNum()+1);
// User toUpdate = new User();
// toUpdate.setId(user.getId());
// toUpdate.setLoginNum(user.getLoginNum());
// userMapper.updateById(toUpdate);
// }
token = System.currentTimeMillis() + "-" + user.getId();
user.setToken(token);
valueOperations.set(token, JSON.toJSONString(user), 1, TimeUnit.DAYS);
session.setAttribute(WebConstants.LOGIN_USER_ID, user.getId());
session.setAttribute(WebConstants.LOGIN_USER_NAME, user.getRealname());
session.setAttribute(WebConstants.LOGIN_USER_TOKEN, token);
session.setAttribute(WebConstants.LOGIN_USER, user);
} else {
log.warn("token不为空");
}
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(token, password, getName());
return simpleAuthenticationInfo;
}
}
授权验证不通过时的filter类 MyAuthenticationFilter
package com.wzu.utilities.config.shiro;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.springframework.data.redis.core.ValueOperations;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.wzu.utilities.entity.po.User;
import com.wzu.utilities.entity.vo.R;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MyAuthenticationFilter extends FormAuthenticationFilter {
public MyAuthenticationFilter(ValueOperations<String, Object> valueOperations, Long timeout) {
this.timeout = timeout;
this.valueOperations = valueOperations;
}
private ValueOperations<String, Object> valueOperations;
private Long timeout;
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setStatus(200);
httpServletResponse.setContentType("application/json;charset=utf8");
Session session = SecurityUtils.getSubject().getSession();
User user = (User)session.getAttribute(WebConstants.LOGIN_USER);
String token = httpServletRequest.getHeader("token");
if (user==null) {
log.debug("没有获取到登录信息,当前token值:{}", token);
//如果session失效,通过header中的token来获取授权信息
if(StringUtils.isNotBlank(token)) {
String userStr = (String)valueOperations.get(token);
if (StringUtils.isBlank(userStr)) {
writeNeedLoginInfo(httpServletResponse);
return false;
}
user = JSONObject.parseObject(userStr, User.class);
session.setAttribute(WebConstants.LOGIN_USER_ID, user.getId());
session.setAttribute(WebConstants.LOGIN_USER_NAME, user.getRealname());
session.setAttribute(WebConstants.LOGIN_USER_TOKEN, token);
session.setAttribute(WebConstants.LOGIN_USER, user);
session.setTimeout(timeout);
log.debug("没有获取到登录信息2,当前token值:{}", token);
return true;
}
} else {
log.debug("获取到登录信息,当前token值:{}", token);
session.setAttribute(WebConstants.LOGIN_USER_ID, user.getId());
session.setAttribute(WebConstants.LOGIN_USER_NAME, user.getRealname());
session.setAttribute(WebConstants.LOGIN_USER_TOKEN, token);
session.setAttribute(WebConstants.LOGIN_USER, user);
session.setTimeout(timeout);
return true;
}
writeNeedLoginInfo(httpServletResponse);
return false;
}
/**
* 返回自定义需要登录的json
* @param httpServletResponse
* @throws IOException
*/
private void writeNeedLoginInfo(HttpServletResponse httpServletResponse) throws IOException {
PrintWriter out = httpServletResponse.getWriter();
out.println(JSON.toJSONString(R.failWithCode(7777, "请先登录")));
out.flush();
out.close();
}
}
登录、登出、登录信息获取的请求代码
@PostMapping("login")
public R<User> login(String username, String password, HttpServletRequest request, HttpServletResponse response) {
Subject subject = SecurityUtils.getSubject();
User user = userMapper.selectOne(new QueryWrapper<User>().eq("username", username));
if (user==null) {
throw new AuthenticationException("用户名或密码错误");
}
UsernamePasswordToken token = new UsernamePasswordToken(username, MD5SecurityUtil.encryptMD5(password));
subject.login(token);
return R.ok(getCurrentLoginUser());
}
@PostMapping("logout")
public R<User> logout() {
Subject subject = SecurityUtils.getSubject();
subject.getSession().removeAttribute(WebConstants.LOGIN_USER);
subject.getSession().removeAttribute(WebConstants.LOGIN_USER_ID);
String token = (String)subject.getSession().removeAttribute(WebConstants.LOGIN_USER_TOKEN);
if(StringUtils.isNotBlank(token)) {
redisTemplate.delete(token);
}
subject.getSession().removeAttribute(WebConstants.LOGIN_USER_NAME);
subject.logout();
return R.ok();
}
@GetMapping("info")
public R<User> getUserInfo() {
User user = getCurrentLoginUser();;
return R.ok(user);
}
public User getCurrentLoginUser() {
User user = (User) SecurityUtils.getSubject().getSession().getAttribute(WebConstants.LOGIN_USER);
if(user==null) {
throw new AccountException();
}
return user;
}
对登录失败时的AccountException进行拦截,返回需要登录的json字符串:
package com.wzu.utilities.web.aspect;
import org.apache.shiro.authc.AccountException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import com.wzu.utilities.entity.enums.ControllerResultEnum;
import com.wzu.utilities.entity.vo.R;
import lombok.extern.slf4j.Slf4j;
/**
* @Author zhangyugu
* @Date 2021/4/24 7:30 下午
* @Version 1.0
*/
@Slf4j
@ControllerAdvice
public class WebExceptionHandler {
@ResponseBody
@ExceptionHandler
public R exceptionHandler(Exception e) {
log.error(e.getMessage(), e);
if(e instanceof AccountException) {
return R.fail(e.getMessage());
}
if(e instanceof UnauthorizedException) {
return R.failWithCode(ControllerResultEnum.USER_HASNO_AUTH.getCode(), e.getMessage());
}
if(e instanceof AuthenticationException) {
return R.failWithCode(ControllerResultEnum.NEED_LOGIN.getCode(), e.getMessage());
}
if(e instanceof UnauthenticatedException) {
return R.failWithCode(ControllerResultEnum.NEED_LOGIN.getCode(), e.getMessage());
}
return R.fail(e.getMessage());
}
}
package com.wzu.utilities.entity.enums;
/**
* @Author zhangyugu
* @Date 2021/4/8 11:47 上午
* @Version 1.0
*/
public enum ControllerResultEnum {
ERROR(300, "运行出错"),
REQUEST_TIMEOUT(408, "请求超时"),
SERVER_DOWN(503,"服务器超负载,或者停机维护"),
NEED_LOGIN(7777, "需要登录"),
USER_HASNO_AUTH(1003, "用户权限不足"),
VIOLATION_EXCEPTION(1200, "验证数据出现错误!"),
RESULT_NULL(1300,"结果为空!"),
FILE_UPLOAD_PATH_NOT_EXIST(1503,"文件路径不存在");
Integer code;
String message;
ControllerResultEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
请求增加角色/权限验证
ShiroConfig配置类增加如下Bean
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 以下两个bean(其中DefaultAdvisorAutoProxyCreator可选)
* 用于启用 @RequiresRoles 和 @RequiresPermissions 注解
* @return
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SessionDAO sessionDAO) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager(sessionDAO));
return authorizationAttributeSourceAdvisor;
}
在CustomRealm中增加角色、权限的设置
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String token = (String) principals.getPrimaryPrincipal();
String userStr = valueOperations.get(token);
if (StringUtils.isBlank(userStr)) {
throw new AccountException("登录失效,请重新登录");
}
User user = JSONObject.parseObject(userStr, User.class);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(Sets.newHashSet(user.getRole()));
//TODO:权限码 info.setStringPermissions();
return info;
}
在Controller类或其方法上增加注解
@RequiresRoles("ADMIN") //验证角色
@RequiresRoles(value= {"ADMIN", "SUPER_ADMIN"}, logical=Logical.OR) //验证角色,ADMIN或者SUPER_ADMIN,注意需要用"或"
@RequiresPermissions("ADD_USER") //验证权限
@RequiresPermissions(value= {"ADD_USER", "DELETE_USER"}, logical=Logical.OR) //验证权限, ADD_USER 或 DELETE_USER