1.设计方案说明
前后端分离项目,使用shiro进行权限校验,shiro相关组件本文不做说明,已经有很多文章说的很详细了.
前后端分离的项目中传统的session方式会存在不一致的问题,所以就引入jwt,利用生成的token信息进行用户信息的认证与授权.后台接口中请求头通过传递token标识用户身份,token的有效存储使用缓存进行存储.
用户认证与授权过程中会获取认证与授权信息,shiro支持进行缓存,这里使用第三方缓存,常用的有redis、ehcache,这里使用redis;
自定义jwt过滤器,让每次请求都走自定义的校验逻辑,主要是重写executeLogin方法,每个请求都进行认证与授权.
2.shiro请求过程梳理(结合源码)
1.shiro请求路径图
对部分逻辑进行说明:
1.subject.login中获取用户认证登录信息:
首先从缓存中取,如果缓存中获取不到,就去自定义的customRealm中进行登录认证(认证完成之后将认证信息添加到缓存中以便下次调用).AuthenticatingRealm.java中getAuthenticationInfo
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 从缓存中获取,如果获取不到就进行自定义的ream中进行登录认证
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
//otherwise not cached, perform the lookup:
info = doGetAuthenticationInfo(token);
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {
cacheAuthenticationInfoIfPossible(token, info);
}
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}
if (info != null) {
// 校验用户密码信息,此处有重写方法进行按照自定义的密码校验
assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
return info;
}
// 从缓存中获取用户认证登录信息
private AuthenticationInfo getCachedAuthenticationInfo(AuthenticationToken token) {
AuthenticationInfo info = null;
Cache<Object, AuthenticationInfo> cache = getAvailableAuthenticationCache();
if (cache != null && token != null) {
log.trace("Attempting to retrieve the AuthenticationInfo from cache.");
Object key = getAuthenticationCacheKey(token);
info = cache.get(key);
if (info == null) {
log.trace("No AuthorizationInfo found in cache for key [{}]", key);
} else {
log.trace("Found cached AuthorizationInfo for key [{}]", key);
}
}
return info;
}
// 走认证逻辑之后将认证信息添加到缓存中,以便下次请求调用
private void cacheAuthenticationInfoIfPossible(AuthenticationToken token, AuthenticationInfo info) {
if (!isAuthenticationCachingEnabled(token, info)) {
log.debug("AuthenticationInfo caching is disabled for info [{}]. Submitted token: [{}].", info, token);
//return quietly, caching is disabled for this token/info pair:
return;
}
Cache<Object, AuthenticationInfo> cache = getAvailableAuthenticationCache();
if (cache != null) {
Object key = getAuthenticationCacheKey(token);
cache.put(key, info);
log.trace("Cached AuthenticationInfo for continued authentication. key=[{}], value=[{}].", key, info);
}
}
2.授权信息验证:shiro拦截器中判断当前方法上是否有五种权限注解,如果有则进行权限校验,没有直接访问接口.AnnotationsAuthorizingMethodInterceptor.assertAuthorized中实现(此处注解都是基于对应的拦截器).
protected void assertAuthorized(MethodInvocation methodInvocation) throws AuthorizationException {
//default implementation just ensures no deny votes are cast:
// 获取shiro默认支持的五种权限注解
Collection<AuthorizingAnnotationMethodInterceptor> aamis = getMethodInterceptors();
if (aamis != null && !aamis.isEmpty()) {
for (AuthorizingAnnotationMethodInterceptor aami : aamis) {
// 判断当前请求的方法上是否有上面五种权限注解,如果有则进行权限校验
if (aami.supports(methodInvocation)) {
aami.assertAuthorized(methodInvocation);
}
}
}
}
3.权限校验方式:首先充缓存中获取授权信息,如果没有则进入到自定义的授权方法(授权完成之后将授权信息添加到缓存中以便下次调用)中,然后进行权限校验
public boolean isPermitted(PrincipalCollection principals, Permission permission) {
// 获取用户授权信息
AuthorizationInfo info = getAuthorizationInfo(principals);
return isPermitted(permission, info);
}
// 根据用户授权信息判断请求权限的具体实现
protected boolean isPermitted(Permission permission, AuthorizationInfo info) {
Collection<Permission> perms = getPermissions(info);
if (perms != null && !perms.isEmpty()) {
for (Permission perm : perms) {
if (perm.implies(permission)) {
return true;
}
}
}
return false;
}
// 获取授权信息
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
if (principals == null) {
return null;
}
AuthorizationInfo info = null;
if (log.isTraceEnabled()) {
log.trace("Retrieving AuthorizationInfo for principals [" + principals + "]");
}
// 首先从缓存中取
Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();
if (cache != null) {
if (log.isTraceEnabled()) {
log.trace("Attempting to retrieve the AuthorizationInfo from cache.");
}
Object key = getAuthorizationCacheKey(principals);
info = cache.get(key);
if (log.isTraceEnabled()) {
if (info == null) {
log.trace("No AuthorizationInfo found in cache for principals [" + principals + "]");
} else {
log.trace("AuthorizationInfo found in cache for principals [" + principals + "]");
}
}
}
// 缓存中获取不到则从自定义的realm中进行授权
if (info == null) {
// Call template method if the info was not found in a cache
info = doGetAuthorizationInfo(principals);
// If the info is not null and the cache has been created, then cache the authorization info.
if (info != null && cache != null) {
if (log.isTraceEnabled()) {
log.trace("Caching authorization info for principals: [" + principals + "].");
}
Object key = getAuthorizationCacheKey(principals);
// 授权信息添加到缓存中
cache.put(key, info);
}
}
return info;
}
3.实现方案具体代码
redistemplate配置类:
package com.kawaxiaoyu.manage.management.common.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.kawaxiaoyu.manage.management.common.shiroRedis.IncludeShiroFields;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
/**
* @ClassName: RedisTemplateConfig
* @Desc: Redis Template 配置,用于序列化redis中对象问题
* @Author: txm
* @Date: 2021/5/21 10:24
**/
@Component
public class RedisTemplateConfig {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 自定义的string序列化器和fastjson序列化器
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// 使用Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 过滤未知字段
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
//只序列化必要shiro字段
String [] needSerialize = {"realmPrincipals"};
objectMapper.addMixIn(SimplePrincipalCollection.class, IncludeShiroFields.class);
objectMapper.setFilters(new SimpleFilterProvider().addFilter("shiroFilter", SimpleBeanPropertyFilter.filterOutAllExcept(needSerialize)));
// 此项必须配置,否则会报java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // 设置缓存存储对象
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); // 设置缓存存储对象
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
自定义realm:
package com.kawaxiaoyu.manage.management.common.shiro;
import com.kawaxiaoyu.manage.management.api.user.entity.ManageUser;
import com.kawaxiaoyu.manage.management.api.user.service.impl.ManageUserServiceImpl;
import com.kawaxiaoyu.manage.management.api.user.vo.LoginReqParam;
import com.kawaxiaoyu.manage.management.common.config.ManageConfig;
import com.kawaxiaoyu.manage.management.common.constant.RedisConstant;
import com.kawaxiaoyu.manage.management.common.excption.BussinessExcption;
import com.kawaxiaoyu.manage.management.common.shiroRedis.RedisCache;
import io.jsonwebtoken.Claims;
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.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.time.Duration;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @ClassName: CustomRealm
* @Desc: 自定义realm
* @Author: txm
* @Date: 2021/8/22 10:25
**/
public class CustomRealm extends AuthorizingRealm {
private static final Logger log= LoggerFactory.getLogger(CustomRealm.class);
@Autowired
private ManageUserServiceImpl manageUserService;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private ManageConfig manageConfig;
public CustomRealm(){
//这里使用我们自定义的Matcher验证接口
this.setCredentialsMatcher(new JWTCredentialsMatcher());
this.setAuthenticationCache(new RedisCache<>("authen"));
this.setAuthorizationCache(new RedisCache<>("author"));
// 设置对认证信息进行缓存
this.setAuthenticationCachingEnabled(true);
}
/**
* 必须重写此方法,不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* shiro 身份验证
* @param token
* @return boolean
* @throws AuthenticationException 抛出的异常将有统一的异常处理返回给前端
*
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)throws AuthenticationException {
// 正常情况下doGetAuthenticationInfo方法只会执行一次,并且从redis中会获取到用户信息,此处登录业务正常情况下不会调用
ManageUser manageUser = (ManageUser)redisTemplate.opsForValue().get(String.format(RedisConstant.MANAGELOGIN_USERID, ((JwtToken) token).getUserId()));
if(manageUser == null){
String tokenStr = ((JwtToken) token).getToken();
if(StringUtils.isEmpty(tokenStr)) throw new BussinessExcption("传递token为空");
Claims claims = JwtUtil.parseJWT(tokenStr, manageConfig.getTokenSecret());
String username = claims.getSubject();
String password = (String) claims.get("password");
LoginReqParam loginReqParam = new LoginReqParam();
loginReqParam.setUsername(username);
loginReqParam.setPassword(password);
manageUser = manageUserService.login(loginReqParam);
}
// Object principal: 标识用户唯一的身份,比如说用户名或是邮箱, Object credentials---用户的安全凭证,比如说密码,String realmName-- 自定义realm
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
manageUser.getUserId(), manageUser.getPassword(), this.getName());
return simpleAuthenticationInfo;
}
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = null;
Long userId =(Long) principals.getPrimaryPrincipal();
// 从缓存中获取用户角色以及权限信息
String manageMenuKey = String.format(RedisConstant.MANAGEMENU_USERID, userId);
List<String> manageMenuList = redisTemplate.opsForList().range(manageMenuKey, 0, -1);
if(CollectionUtils.isEmpty(manageMenuList)){
Set<String> menuSet = manageUserService.findMenuSet(userId);
manageMenuList = menuSet.stream().collect(Collectors.toList());
// 用户拥有角色信息添加到缓存中,设置默认值7天
redisTemplate.opsForList().leftPushAll(manageMenuKey,manageMenuList);
redisTemplate.expire(manageMenuKey, Duration.ofMillis(manageConfig.getTokenExpireTime()));
}
log.info("用户id:{},拥有的权限:{}", userId,manageMenuList);
try {
info = new SimpleAuthorizationInfo();
//info.setRoles(roles); // 放入角色信息
info.setStringPermissions(manageMenuList.stream().collect(Collectors.toSet())); // 放入权限信息
}catch (Exception e){
throw new AuthenticationException("授权失败!");
}
return info;
}
}
自定义用户密码校验
package com.kawaxiaoyu.manage.management.common.shiro;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.CredentialsMatcher;
/**
* @ClassName: JWTCredentialsMatcher
* @Desc: jwt验证匹配器
* @Author: txm
* @Date: 2021/8/22 10:28
**/
public class JWTCredentialsMatcher implements CredentialsMatcher {
/**
* @Author: txm
* @Description: 重写校验用户密码的逻辑
* @Param: [authenticationToken, authenticationInfo]
* @return: boolean
* @Date: 2021/8/28 14:04
**/
@Override
public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) {
String token = (String) ((JwtToken)authenticationToken).getToken();
String password = (String)authenticationInfo.getCredentials();
//得到DefaultJwtParser
Boolean verify = JwtUtil.isVerify(token, password);
return verify;
}
}
自定义jwt过滤器
package com.kawaxiaoyu.manage.management.common.shiro;
import com.alibaba.fastjson.JSONObject;
import com.kawaxiaoyu.manage.management.common.constant.Constants;
import com.kawaxiaoyu.manage.management.common.excption.BussinessExcption;
import com.kawaxiaoyu.manage.management.common.util.DateTimeUtil;
import io.jsonwebtoken.Claims;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
/**
* @ClassName: JwtFilter
* @Desc: JwtFilter
* @Author: txm
* @Date: 2021/8/22 10:56
**/
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
* 应用的HTTP方法列表配置基本身份验证筛选器。
* 获取 request 请求 拒绝拦截登录请求
* 执行登录认证方法
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String requestURI = httpServletRequest.getRequestURI();
if (requestURI.equals(JwtUtil.loginPath) ) {
return true;
} else {
try {
boolean result = executeLogin(request, response);
return result;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
/**
* Authorization携带的参数为token
* JwtToken实现了AuthenticationToken接口封装了token参数
* 通过getSubject方法获取 subject对象
* login()发送身份验证
* @param request
* @param response
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
try {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("token");
JwtToken jwtToken = new JwtToken(token);
if(token != null){
// 对传递过来的token进行解密,获取对象信息
Claims claims = JwtUtil.parseJWT(token, JwtUtil.secretKey);
String userId =(String) claims.get("userId");
// 校验token是否有效
boolean verifyToken = JwtUtil.verifyToken(token, Long.valueOf(userId));
if(verifyToken) throw new BussinessExcption("用户认证信息失败:用户token身份过期,请重新登录");
jwtToken.setUserId(Long.valueOf(userId));
}
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
Subject subject = getSubject(request, response);
subject.login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
} catch (Exception e) {
// 过滤器异常返回前端处理方案
JSONObject responseJSONObject = new JSONObject();
responseJSONObject.put("success","false");
responseJSONObject.put("msg",e.getMessage());
responseJSONObject.put("time", DateTimeUtil.getCurrentTime());
response.setCharacterEncoding(Constants.UTF8);
response.setContentType(Constants.CONTENT_TYPE);
OutputStream os = response.getOutputStream();
os.write(JSONObject.toJSONBytes(responseJSONObject));
os.flush();
os.close();
}
return false;
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
自定义token
package com.kawaxiaoyu.manage.management.common.shiro;
import org.apache.shiro.authc.AuthenticationToken;
/**
* @ClassName: JwtToken
* @Desc: 自定义jwtToken
* @Author: txm
* @Date: 2021/8/22 10:33
**/
public class JwtToken implements AuthenticationToken {
private String token;
private long userId;
public long getUserId() {
return userId;
}
public void setUserId(long userId) {
this.userId = userId;
}
public JwtToken(String token) {
this.token = token;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return this.getUserId();
}
@Override
public Object getCredentials() {
return null;
}
}
自定义jwt工具类
package com.kawaxiaoyu.manage.management.common.shiro;
import com.kawaxiaoyu.manage.management.api.user.entity.ManageUser;
import com.kawaxiaoyu.manage.management.api.user.vo.LoginReqParam;
import com.kawaxiaoyu.manage.management.common.config.ManageConfig;
import com.kawaxiaoyu.manage.management.common.constant.RedisConstant;
import com.kawaxiaoyu.manage.management.common.excption.BussinessExcption;
import com.kawaxiaoyu.manage.management.common.util.PasswordUtil;
import com.kawaxiaoyu.manage.management.common.util.SpringUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @ClassName: JwtUtil
* @Desc: JwtUtil
* @Author: txm
* @Date: 2021/8/22 10:34
**/
public class JwtUtil {
public static final String secretKey;
public static final String userSalt;
public static final RedisTemplate redisTemplate;
public static final long tokenExpireTime;
public static final String loginPath;
static {
// 获取token加密签名
ManageConfig manageConfig=SpringUtils.getBean("manageConfig");
secretKey = manageConfig.getTokenSecret();
userSalt = manageConfig.getUserSalt();
tokenExpireTime = manageConfig.getTokenExpireTime();
loginPath = manageConfig.getLoginPath();
// 获取redisTemplate
redisTemplate=SpringUtils.getBean("redisTemplate");
}
/**
* 用户登录成功后生成Jwt
* 使用Hs256算法 私匙使用用户密码
*
* @param ttlMillis jwt过期时间
* @param loginReqParam 登录成功的user对象
* @return
*/
public static String createJWT(long ttlMillis, LoginReqParam loginReqParam) {
//指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//生成JWT的时间
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
// 创建私有声明
Map<String, Object> map = new HashMap<String, Object>();
map.put("username", loginReqParam.getUsername());
map.put("password", loginReqParam.getPassword());
map.put("userId", String.valueOf(loginReqParam.getUserId()));
//生成签发人
String subject = loginReqParam.getUsername();
//这里其实就是new一个JwtBuilder,设置jwt的body
JwtBuilder builder = Jwts.builder()
//如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(map)
//设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
.setId(UUID.randomUUID().toString())
//iat: jwt的签发时间
.setIssuedAt(now)
//代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
.setSubject(subject)
//设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, secretKey);
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
//设置过期时间
builder.setExpiration(exp);
}
return builder.compact();
}
/**
* 校验token
* 在这里可以使用官方的校验,我这里校验的是token中携带的密码于数据库一致的话就校验通过
* @param token
* @return
*/
public static Boolean isVerify(String token, String password) {
try {
//得到DefaultJwtParser
Claims claims = Jwts.parser()
//设置签名的秘钥
.setSigningKey(secretKey)
//设置需要解析的jwt
.parseClaimsJws(token).getBody();
String encryptPassword = PasswordUtil.encrypt((String) claims.get("password"), userSalt);
if (encryptPassword.equals(password)) {
return true;
}
} catch (Exception exception) {
return false;
}
return false;
}
/**
* Token的解密
* @param token 加密后的token
* @param secret 签名秘钥,和生成的签名的秘钥一模一样
* @return
*/
public static Claims parseJWT(String token, String secret) {
//得到DefaultJwtParser
Claims claims = Jwts.parser()
//设置签名的秘钥
.setSigningKey(secret)
//设置需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}
/**
* Token有效性校验
* @param token 加密后的token
* @param userId 签名秘钥,和生成的签名的秘钥一模一样
* @return
*/
public static boolean verifyToken(String token,long userId){
boolean isExpired=false;
// 校验token是否失效,从缓存中获取token信息
String redisToken = (String) redisTemplate.opsForValue().get(String.format(RedisConstant.MANAGE_TOKEN, userId));
if(StringUtils.isEmpty(redisToken) || !token.equals(redisToken)) {
isExpired=true;
}
return isExpired;
}
/**
* 获取用户信息
* @param userId 签名秘钥,和生成的签名的秘钥一模一样
* @return
*/
public static ManageUser getUserInfo(long userId){
ManageUser manageUser = (ManageUser) redisTemplate.opsForValue().get(String.format(RedisConstant.MANAGELOGIN_USERID, userId));
if(manageUser == null) throw new BussinessExcption("用户信息获取失败,请重新登录");
return manageUser;
}
}
自定义shiro配置类:
package com.kawaxiaoyu.manage.management.common.shiro;
import com.kawaxiaoyu.manage.management.common.shiroRedis.RedisCacheManager;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @ClassName: ShiroConfiguration
* @Desc: shiro配置类
* @Author: txm
* @Date: 2021/8/22 10:49
**/
@Configuration
public class ShiroConfiguration {
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/user/login", "anon");
// 对静态资源设置匿名访问
filterChainDefinitionMap.put("/favicon.ico**", "anon");
filterChainDefinitionMap.put("/html/**", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/docs/**", "anon");
filterChainDefinitionMap.put("/fonts/**", "anon");
filterChainDefinitionMap.put("/images/**", "anon");
filterChainDefinitionMap.put("/ajax/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
// knife4j权限放开
// filterChainDefinitionMap.put("/swagger/**", "anon");
filterChainDefinitionMap.put("/doc.html", "anon");
//swagger接口权限 开放
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
filterChainDefinitionMap.put("/v2/**", "anon");
filterChainDefinitionMap.put("/configuration/**", "anon");
filterChainDefinitionMap.put("/service-worker.js", "anon");
filterChainDefinitionMap.put("/index.html", "anon");
filterChainDefinitionMap.put("/manifest.json", "anon");
filterChainDefinitionMap.put("/robots.txt", "anon");
filterChainDefinitionMap.put("/precache-manifest.eea302037a9c2783bdf341d6c2dd2ca2.js", "anon");
// filterChainDefinitionMap.put("/user/logout", "anon");
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<String, Filter>();
filterMap.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
//<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边,不使用自定义的jwt过滤器
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean("securityManager")
public SecurityManager securityManager(CustomRealm customRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm);
/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
// 注入缓存管理器;不使用ehcache
// securityManager.setCacheManager(getEhCacheManager());
// 缓存使用redis
securityManager.setCacheManager(new RedisCacheManager());
return securityManager;
}
/**
* 创建自定义的CustomRealm @bean
*/
@Bean("customRealm")
public CustomRealm shiroRealm() {
CustomRealm customRealm = new CustomRealm();
// 使用redis进行缓存认证以及授权信息
// 开启全局缓存
customRealm.setCachingEnabled(true);
// 开启认证缓存
customRealm.setAuthenticationCachingEnabled(true);
// 开启授权缓存管理
customRealm.setAuthorizationCachingEnabled(true);
// 开启Redis缓存
customRealm.setCacheManager(new RedisCacheManager());
return customRealm;
}
//自动创建代理,没有这个鉴权可能会出错
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
autoProxyCreator.setProxyTargetClass(true);
return autoProxyCreator;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
*
* @param
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
自定义shiro工具类:
package com.kawaxiaoyu.manage.management.common.shiro;
import com.kawaxiaoyu.manage.management.api.user.entity.ManageUser;
import com.kawaxiaoyu.manage.management.common.util.CheckEmptyUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.BeanUtils;
/**
* @ClassName: ShiroUtils
* @Desc: shiro工具类
* @Author: txm
* @Date: 2021/6/23 16:42
**/
public class ShiroUtils
{
public static Subject getSubject()
{
return SecurityUtils.getSubject();
}
public static ManageUser getSysUser() {
ManageUser user = null;
Object obj = getSubject().getPrincipal();
if (CheckEmptyUtil.isNotNull(obj))
{
user = new ManageUser();
BeanUtils.copyProperties(obj,user);
}
return user;
}
public static Long getUserId()
{
return getSysUser().getUserId().longValue();
}
public static String getLoginName()
{
return getSysUser().getLoginName();
}
public static String getIp()
{
return getSubject().getSession().getHost();
}
}
4.反思与总结
问题主要出现在过滤器中出现的异常如何进行返回到前端(不像controller层抛出异常能使用使),这里提供的方案是流的方式组装数据返回.
对于shiro接入第三方缓存用时较多,通过本次可以体会一下经典框架如何进行自定义接入,实现原理都大概相同.
认证信息进行反序列化的地方出现过问题卡了很长时间,Jackson支持对指定的属性进行过滤,设置方式可以参考Redistemplate配置类.
关于实现的细节问题,在代码中已做补充说明!如有疑问或不当之处欢迎评论区留言.