redis工具类 https://www.jianshu.com/p/1b3f33a045bf
- 相关依赖
org.apache.shiro
shiro-spring
1.4.0
com.auth0
java-jwt
3.9.0
org.springframework.boot
spring-boot-starter-data-redis
cn.hutool
hutool-all
5.6.3
com.alibaba
fastjson
1.2.76
- yml配置jwt
jwt:
authoritiesKey: auth
# 密匙KEY
secret: KeXu6IgYf7xaPe4jpw
# HeaderKEY
tokenHeader: Token
# jwt过期时间 单位秒 1天后过期=86400 7天后过期=604800
expiration: 1800
# 放入redis过期时间 单位秒 1天后过期=86400 7天后过期=604800
redisExpiration: 1800
获取yml里的jwt配置信息
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* JWT配置类
*/
@Getter
@Component
@ConfigurationProperties(prefix = "jwt")
public class JWTConfig {
public static String authoritiesKey;
/**
* 密钥KEY
*/
public static String secret;
/**
* TokenKey
*/
public static String tokenHeader;
/**
* 过期时间
*/
public static Integer expiration;
/**
* redis过期时间
*/
public static Integer redisExpiration;
public void setAuthoritiesKey(String authoritiesKey) {
this.authoritiesKey = authoritiesKey;
}
public void setSecret(String secret) {
this.secret = secret;
}
public void setTokenHeader(String tokenHeader) {
this.tokenHeader = tokenHeader;
}
public void setExpiration(Integer expiration) {
this.expiration = expiration;
}
public void setRedisExpiration(Integer redisExpiration) {
this.redisExpiration = redisExpiration;
}
}
- JwtTokenUtil用于解析和生成token
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.mp.generator.config.jwt.JWTConfig;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.Date;
import java.util.Objects;
public class JwtTokenUtil implements Serializable {
private static final long serialVersionUID = -5625635588908941275L;
public static String generateToken(String username, Long current) {
if (null == current) {
current = System.currentTimeMillis();
}
Algorithm algorithm = Algorithm.HMAC256(JWTConfig.secret);
return JWT.create()
.withClaim(JWTConfig.authoritiesKey, username)
.withClaim("current", current)
.withExpiresAt(new Date(current + JWTConfig.expiration * 1000))
.sign(algorithm);
}
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(JWTConfig.authoritiesKey).asString();
} catch (JWTDecodeException e) {
return null;
}
}
public static String getToken() {
String token;
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
token = request.getHeader(JWTConfig.tokenHeader);
if (StringUtils.isBlank(token)) {
return "";
}
return token;
}
public static boolean isExpired(String token) {
DecodedJWT jwt = JWT.decode(token);
Date expiration = jwt.getExpiresAt();
return expiration.before(new Date());
}
public static boolean verify(String token) {
try {
//解密
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(JWTConfig.secret)).build();
verifier.verify(token);
return true;
} catch (Exception e) {
return false;
}
}
public static Long getCurrent(String token){
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("current").asLong();
}catch (Exception e){
return null;
}
}
}
- UserToken,TokenFilter过滤器中使用,传递token信息
import org.apache.shiro.authc.AuthenticationToken;
import java.io.Serializable;
public class UserToken implements AuthenticationToken, Serializable {
private static final long serialVersionUID = 1841491628743017587L;
private final String token;
public UserToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
- 实现过滤器TokenFilter,判断用户是否登录
import com.mp.generator.config.jwt.JWTConfig;
import com.mp.generator.constants.RedisConstant;
import com.mp.generator.utils.JwtTokenUtil;
import com.mp.generator.utils.RedisUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
public class TokenFilter extends BasicHttpAuthenticationFilter {
private final Logger log = LoggerFactory.getLogger(this.getClass());
/**
* 如果带有 com.token,则对 com.token 进行检查,否则直接通过
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//判断请求的请求头是否带上 "Token"
if (isLoginAttempt(request, response)) {
//如果存在,则进入 executeLogin 方法执行登入,检查 com.token 是否正确
try {
return executeLogin(request, response);
} catch (Exception e) {
log.error("shiro验证异常 {}", Arrays.asList(e.getStackTrace()));
responseError(response, "shiro fail");
return false;
}
}
//1.如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 com.token,直接返回 true
//2.如果请求头不存在 Token,并且返回false,说明走我们的过滤器时必须带token
return true;
}
/**
* 判断用户是否想要登入。
* 检测 header 里面是否包含 JWTConfig.tokenHeader 字段
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader(JWTConfig.tokenHeader);
return StringUtils.isNotBlank(token);
}
/**
* 执行登陆操作
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(JWTConfig.tokenHeader);
UserToken userToken = new UserToken(token);
try {
Subject subject = this.getSubject(request, response);
subject.login(userToken);
return this.onLoginSuccess(userToken, subject, request, response);
} catch (AuthenticationException var5) {
return this.onLoginFailure(userToken, var5, request, response);
}
}
/**
* 对跨域提供支持
*/
@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错误
*
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
try {
String token = ((HttpServletRequest) request).getHeader(JWTConfig.tokenHeader);
log.info("--------token过期:{}--------", token);
} catch (Exception e) {
log.info("--------token不存在--------");
}
this.sendChallenge(request, response);
this.responseError(response, "token已过期或不存在!");
return Boolean.FALSE;
}
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
log.info("--------onLoginSuccess--------");
String tokenPrincipal = (String) token.getPrincipal();
if (StringUtils.isNotBlank(tokenPrincipal)) {
if (JwtTokenUtil.verify(tokenPrincipal)) {
return true;
} else {
return refreshToken(request, response);
}
}
return true;
}
/**
* 将非法请求跳转到 /unauthorized/**
*/
private void responseError(ServletResponse response, String message) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
response.reset();
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json; charset=utf-8");
String jsonStr = "{\"result\":\"FAILURE\",\"code\":401,\"message\":\"" + message + "\"}";
try (PrintWriter out = response.getWriter()) {
out.append(jsonStr);
} catch (IOException e) {
log.error("sendChallenge error,can not resolve httpServletResponse");
}
}
public T getBean(Class clazz, HttpServletRequest request) {
WebApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
return applicationContext.getBean(clazz);
}
// 刷新token
private boolean refreshToken(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
RedisUtil redisUtil = getBean(RedisUtil.class, req);
// 获取传递过来的accessToken
String accessToken = req.getHeader(JWTConfig.tokenHeader);
// 获取token里面的用户名
String username = JwtTokenUtil.getUsername(accessToken);
// 判断refreshToken是否过期了,过期了那么所含的username的键不存在
if (redisUtil.hasKey(RedisConstant.CACHE_PREFIX + RedisConstant.CACHE_USER_ENTER + username)) {
// 判断refresh的时间节点和传递过来的accessToken的时间节点是否一致,不一致校验失败
Long current = (Long) redisUtil.getCacheObject(RedisConstant.CACHE_PREFIX + RedisConstant.CACHE_USER_ENTER + username);
if (Objects.equals(JwtTokenUtil.getCurrent(accessToken), current)) {
// 获取当前时间节点
long currentTimeMillis = System.currentTimeMillis();
// 生成刷新的token
String token = JwtTokenUtil.generateToken(username, currentTimeMillis);
// 刷新redis里面的refreshToken,过期时间是(JWTConfig.expiration + 30*60)min
redisUtil.setCacheObject(RedisConstant.CACHE_PREFIX + RedisConstant.CACHE_USER_ENTER + username, currentTimeMillis, JWTConfig.expiration + JWTConfig.redisExpiration, TimeUnit.SECONDS);
// 最后将刷新的AccessToken存放在Response的Header中的JWTConfig.tokenHeader字段返回
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader(JWTConfig.tokenHeader, token);
httpServletResponse.setHeader("Access-Control-Expose-Headers", JWTConfig.tokenHeader);
return true;
}
}
return false;
}
}
- 新建ShiroConfig,添加自己的过滤规则
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
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;
@Configuration
@Slf4j
public class ShiroConfig {
/**
* 先走 com.**.TokenFilter ,然后 com.**.TokenFilter 如果检测到请求头存在token,则用token 去 login,走 Realm 去验证
*/
@Bean
public ShiroFilterFactoryBean factory(SecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 添加自己的过滤器并且取名为token
Map filterMap = new HashMap<>();
//设置我们自定义的Token过滤器
filterMap.put("token", new TokenFilter());
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
// 设置无权限时跳转的 url;
factoryBean.setUnauthorizedUrl("/unauthorized/无权限");
Map filterRuleMap = new LinkedHashMap<>();
filterRuleMap.put("/unauthorized/**", "anon");
filterRuleMap.put("/api/login/**", "anon");
filterRuleMap.put("/**", "token");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
/**
* 注入 securityManager
*/
@Bean
public SecurityManager securityManager(UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置自定义 realm.
securityManager.setRealm(userRealm);
/*
* 关闭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);
return securityManager;
}
/**
* 添加注解支持
*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
- UserRealm,对登录用户和用户的权限进行验证
import cn.hutool.core.util.ObjectUtil;
import com.mp.generator.entity.SysMenu;
import com.mp.generator.entity.SysRole;
import com.mp.generator.entity.UserInfo;
import com.mp.generator.service.UserInfoService;
import com.mp.generator.utils.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.util.stream.Collectors;
@Slf4j
@Component
public class UserRealm extends AuthorizingRealm {
@Lazy
@Autowired
private UserInfoService userInfoService;
/**
* 必须重写此方法,不然会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UserToken;
}
/**
* 授权
* 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("授权验证->doGetAuthenticationInfo()");
String token = principals.toString();
String username = JwtTokenUtil.getUsername(token);
UserInfo userInfo = userInfoService.getUserByUsername(username);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//查询数据库来获取用户的角色
info.addRoles(userInfo.getRoleList().stream().map(SysRole::getRoleName).collect(Collectors.toSet()));
//查询数据库来获取用户的权限
info.addStringPermissions(userInfo.getMenuList().stream().map(SysMenu::getPermission).collect(Collectors.toSet()));
return info;
}
/**
* 身份验证
* 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
*
* @param authenticationToken token
* @return SimpleAuthenticationInfo
* @throws AuthenticationException 登录异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("登录验证->doGetAuthenticationInfo()");
String token = (String) authenticationToken.getCredentials();
log.info("当前token : {}", token);
String username = JwtTokenUtil.getUsername(token);
if (ObjectUtil.isNull(username)) {
throw new AuthenticationException("登录过期,请重新登录!");
}
UserInfo userInfo = userInfoService.getUserByUsername(username);
if (null == userInfo) {
throw new AuthenticationException("该用户不存在");
}
log.info("当前用户 : {}", userInfo);
return new SimpleAuthenticationInfo(token, token, "UserRealm");
}
}
- 登录实现
import com.mp.generator.config.jwt.JWTConfig;
import com.mp.generator.constants.RedisConstant;
import com.mp.generator.dto.request.LoginRequest;
import com.mp.generator.entity.UserInfo;
import com.mp.generator.service.LoginService;
import com.mp.generator.service.UserInfoService;
import com.mp.generator.utils.JwtTokenUtil;
import com.mp.generator.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private UserInfoService userInfoService;
@Autowired
private RedisUtil redisUtil;
@Override
public String login(LoginRequest request) {
if (StringUtils.isBlank(request.getPassword())) {
throw new RuntimeException("请输入密码");
}
UserInfo userInfo = userInfoService.getUserInfo(request.getUserName());
if (null == userInfo) {
throw new RuntimeException("用户名不存在");
}
String password = new SimpleHash("MD5", request.getPassword(), userInfo.getSalt(), 1024).toHex();
if (!password.equals(userInfo.getPassword())) {
throw new RuntimeException("密码不正确");
}
long currentTimeMillis = System.currentTimeMillis();
String token = JwtTokenUtil.generateToken(userInfo.getUserName(), currentTimeMillis);
redisUtil.setCacheObject(RedisConstant.CACHE_PREFIX + RedisConstant.CACHE_USER_ENTER + userInfo.getUserName(), currentTimeMillis, JWTConfig.expiration + JWTConfig.redisExpiration, TimeUnit.SECONDS);
redisUtil.deleteObject(RedisConstant.CACHE_PREFIX + RedisConstant.CACHE_USER_INFO + userInfo.getUserName());
userInfoService.getUserByUsername(userInfo.getUserName());
return token;
}
}
UserInfoServiceImpl
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mp.generator.config.jwt.JWTConfig;
import com.mp.generator.entity.SysMenu;
import com.mp.generator.entity.SysRole;
import com.mp.generator.entity.UserInfo;
import com.mp.generator.constants.RedisConstant;
import com.mp.generator.mapper.UserInfoMapper;
import com.mp.generator.service.UserInfoService;
import com.mp.generator.utils.JwtTokenUtil;
import com.mp.generator.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class UserInfoServiceImpl extends ServiceImpl implements UserInfoService {
@Autowired
private RedisUtil redisUtil;
@Override
public UserInfo getUserInfo(String username) {
return baseMapper.selectOne(Wrappers.lambdaQuery().eq(UserInfo::getUserName, username).last("LIMIT 1"));
}
@Override
public UserInfo getUserByUsername(String username) {
UserInfo userInfo = (UserInfo) redisUtil.getCacheObject(RedisConstant.CACHE_PREFIX + RedisConstant.CACHE_USER_INFO + username);
if (null == userInfo) {
log.info("redis缓存用户已过期");
userInfo = this.getUserInfo(username);
if (null == userInfo) {
throw new AuthenticationException("用户不存在");
}
List roleList = this.selectSysRoleByUserId(userInfo.getId());
List menuList = this.selectSysMenuByUserId(userInfo.getId());
menuList.add(new SysMenu().setName("添加用户").setPermission("user:add"));
menuList.add(new SysMenu().setName("用户分页").setPermission("user:page"));
userInfo.setRoleList(roleList);
userInfo.setMenuList(menuList);
redisUtil.setCacheObject(RedisConstant.CACHE_PREFIX + RedisConstant.CACHE_USER_INFO + userInfo.getUserName(), userInfo, JWTConfig.expiration + JWTConfig.redisExpiration, TimeUnit.SECONDS);
}
return userInfo;
}
@Override
public UserInfo getCurrentUserInfo() {
return this.getCurrentUserInfo(Boolean.FALSE);
}
@Override
public UserInfo getCurrentUserInfo(Boolean forceRefresh) {
String username = JwtTokenUtil.getUsername(JwtTokenUtil.getToken());
if (Boolean.TRUE.equals(forceRefresh)) {
redisUtil.deleteObject(RedisConstant.CACHE_PREFIX + RedisConstant.CACHE_USER_INFO + username);
}
return this.getUserByUsername(username);
}
@Override
public List selectSysRoleByUserId(Long userId) {
// 具体根据业务实现
return new ArrayList<>();
}
@Override
public List selectSysMenuByUserId(Long userId) {
// 具体根据业务实现
return new ArrayList<>();
}
}