现在主流的单token方案为jwttoken和redis token
常用的跟jwt token集成框架有shrio、spring security、aop切面。redis也能跟这三者集成。跟redis相比,jwt token比较难注销,得等到有效期过了才行,实际根据项目需求来就行。
简单介绍如下,理论就不讲太多,看代码吧。
io.jsonwebtoken
jjwt-api
0.10.7
io.jsonwebtoken
jjwt-impl
0.10.7
runtime
io.jsonwebtoken
jjwt-jackson
0.10.7
runtime
org.apache.shiro
shiro-spring
1.5.3
工具类JwtOperator.java
package com.vvvtimes.demo.util;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.Map;
@Slf4j
@RequiredArgsConstructor
@SuppressWarnings("WeakerAccess")
@Component
public class JwtOperator {
/**
* 秘钥
* - 默认aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt
*/
@Value("${secret:aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt}")
private String secret;
/**
* 有效期,单位秒
* - 默认2周
*/
@Value("${expire-time-in-second:1209600}")
private Long expirationTimeInSecond;
/**
* 从token中获取claim
*
* @param token token
* @return claim
*/
public Claims getClaimsFromToken(String token) {
try {
return Jwts.parser()
.setSigningKey(this.secret.getBytes())
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {
log.error("token解析错误", e);
throw new IllegalArgumentException("Token invalided.");
}
}
/**
* 获取token的过期时间
*
* @param token token
* @return 过期时间
*/
public Date getExpirationDateFromToken(String token) {
return getClaimsFromToken(token)
.getExpiration();
}
/**
* 判断token是否过期
*
* @param token token
* @return 已过期返回true,未过期返回false
*/
private Boolean isTokenExpired(String token) {
Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
/**
* 计算token的过期时间
*
* @return 过期时间
*/
private Date getExpirationTime() {
return new Date(System.currentTimeMillis() + this.expirationTimeInSecond * 1000);
}
/**
* 为指定用户生成token
*
* @param claims 用户信息
* @return token
*/
public String generateToken(Map claims) {
Date createdTime = new Date();
Date expirationTime = this.getExpirationTime();
byte[] keyBytes = secret.getBytes();
SecretKey key = Keys.hmacShaKeyFor(keyBytes);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(createdTime)
.setExpiration(expirationTime)
// 你也可以改用你喜欢的算法
// 支持的算法详见:https://github.com/jwtk/jjwt#features
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
/**
* 判断token是否非法
*
* @param token token
* @return 未过期返回true,否则返回false
*/
public Boolean validateToken(String token) {
return !isTokenExpired(token);
}
/*public static void main(String[] args) {
// 1. 初始化
JwtOperator jwtOperator = new JwtOperator();
jwtOperator.expirationTimeInSecond = 1209600L;
jwtOperator.secret = "aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt";
// 2.设置用户信息
HashMap objectObjectHashMap = Maps.newHashMap();
objectObjectHashMap.put("id", "1");
// 测试1: 生成token
String token = jwtOperator.generateToken(objectObjectHashMap);
// 会生成类似该字符串的内容: eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk4MTcsImV4cCI6MTU2Njc5OTQxN30.27_QgdtTg4SUgxidW6ALHFsZPgMtjCQ4ZYTRmZroKCQ
System.out.println(token);
// 将我改成上面生成的token!!!
String someToken = "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE2OTU4OTc2NTQsImV4cCI6MTY5NzEwNzI1NH0.XK730y8tMwAjjTdLqYV4FArwX-_7l0oUvtDmJX2wmLE";
// 测试2: 如果能token合法且未过期,返回true
Boolean validateToken = jwtOperator.validateToken(someToken);
System.out.println(validateToken);
// 测试3: 获取用户信息
Claims claims = jwtOperator.getClaimsFromToken(someToken);
System.out.println(claims);
// 将我改成你生成的token的第一段(以.为边界)
String encodedHeader = "eyJhbGciOiJIUzI1NiJ9";
// 测试4: 解密Header
byte[] header = Base64.decodeBase64(encodedHeader.getBytes());
System.out.println(new String(header));
// 将我改成你生成的token的第二段(以.为边界)
String encodedPayload = "eyJpZCI6IjEiLCJpYXQiOjE2OTU4OTc2NTQsImV4cCI6MTY5NzEwNzI1NH0";
// 测试5: 解密Payload
byte[] payload = Base64.decodeBase64(encodedPayload.getBytes());
System.out.println(new String(payload));
// 测试6: 这是一个被篡改的token,因此会报异常,说明JWT是安全的
jwtOperator.validateToken("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk3MzIsImV4cCI6MTU2Njc5OTMzMn0.nDv25ex7XuTlmXgNzGX46LqMZItVFyNHQpmL9UQf-aUx");
}*/
}
配置类ShiroBeanConfig
package com.vvvtimes.demo.auth.config;
import com.vvvtimes.demo.auth.ShiroAuthFilter;
import com.vvvtimes.demo.auth.ShiroAuthRealm;
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;
/**
* shrio配置类
*/
@Configuration
public class ShiroBeanConfig {
@Bean("securityManager")
public SecurityManager securityManager(ShiroAuthRealm shiroAuthRealm){
//ShiroAuthRealm是自定义reaml
DefaultWebSecurityManager manager=new DefaultWebSecurityManager();
manager.setRealm(shiroAuthRealm);
return manager;
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilter=new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
//装载自定义的token过滤器
Map tokenFilterMap=new HashMap<>();
tokenFilterMap.put("oauth2",new ShiroAuthFilter());
shiroFilter.setFilters(tokenFilterMap);
//普通路径过滤规则
Map filterMap = new LinkedHashMap<>();
//测试相关
filterMap.put("/test", "anon");
//登录相关
filterMap.put("/login/doLogin", "anon");
filterMap.put("/login/logout", "anon");
filterMap.put("/login/noLogin", "anon");
filterMap.put("/login/captcha", "anon");
//临时测试
//filterMap.put("/user/addUser", "anon");
//自定义的token过滤器拦截其他任何请求
filterMap.put("/**", "oauth2");
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
shrio过滤器类
package com.vvvtimes.demo.auth;
import cn.hutool.json.JSONUtil;
import com.vvvtimes.demo.common.dto.RestResponse;
import com.vvvtimes.demo.vo.UserInfo;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;
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.IOException;
/**
* Shiro过滤器类
*/
public class ShiroAuthFilter extends AuthenticatingFilter {
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
String token = getToken((HttpServletRequest) servletRequest);
if(StringUtils.hasText(token)){
//ShiroAuthToken是自己定义实现AuthenticationToken接口的token
return new ShiroAuthToken(token);
}
//executeLogin调用该方法token==null就抛异常,我们在onAccessDenied入口
//重写方法没有token就提前返回并携带原因
return null;
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return false;
}
/**
* 登录失败,重写方法返回失败原因
* @param token
* @param e
* @param request
* @param response
* @return false
*/
@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", ((HttpServletRequest)request).getHeader("Origin"));
httpResponse.setCharacterEncoding("utf-8");
httpResponse.setHeader("ContentType","application/json;charset=utf-8");
httpResponse.setContentType("application/json;charset=utf-8");
try {
//处理登录失败的异常
Throwable throwable = e.getCause() == null ? e : e.getCause();
//返回401 new JsonResult.error(throwable.getMessage())
RestResponse restResponse = new RestResponse<>();
restResponse.setStatus(401);
restResponse.setMessage("token校验失败");
String json = JSONUtil.toJsonStr(restResponse);
httpResponse.getWriter().print(json);
} catch (IOException e1) {
e1.printStackTrace();
}
return false;
}
/**
* 程序入口 检验token是否携带,没携带就返回提示没有token
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
//获取用户token
String token = getToken((HttpServletRequest) servletRequest);
//如果token不存在提前返回401
if(!StringUtils.hasText(token)){
HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", ((HttpServletRequest)servletRequest).getHeader("Origin"));
//new JsonResult.error(invalid token)
httpResponse.setCharacterEncoding("utf-8");
httpResponse.setHeader("ContentType","application/json;charset=utf-8");
httpResponse.setContentType("application/json;charset=utf-8");
RestResponse restResponse = new RestResponse<>();
restResponse.setStatus(401);
restResponse.setMessage("token不存在");
String json = JSONUtil.toJsonStr(restResponse);
httpResponse.getWriter().print(json);
return false;
}
//executeLogin方法会校验createToken是否返回了非null AuthenticationToken
//上面我们提前校验提前返回信息给前端
return executeLogin(servletRequest, servletResponse);
}
// 处理跨域预检请求 preflight
@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"));
// 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* 方法获取用户的token
* @param request
* @return token
*/
public String getToken(HttpServletRequest request){
//获取请求头
String token = request.getHeader("token");
//没有请求头就从参数里拿
if (!StringUtils.hasText(token)){
token=request.getParameter("token");
}
return token;
}
}
shrio realm类
package com.vvvtimes.demo.auth;
import com.vvvtimes.demo.mapper.UserMapper;
import com.vvvtimes.demo.util.JwtOperator;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
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.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* shrioRealm类
*/
@Component
@Slf4j
public class ShiroAuthRealm extends AuthorizingRealm {
@Resource
private UserMapper userMapper;
@Autowired
private JwtOperator jwtOperator;
/**
* 判断是否支持token的类型 ****important****
* 每一个Ream都有一个supports方法,用于检测是否支持此Token,默认的采用了return false
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof ShiroAuthToken;
}
/**
* 授权 集合会与 @RequiresPermissions()声明的权限方法匹配,通常不调用权限的方法不会执行
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
Map user = (Map) principalCollection.getPrimaryPrincipal();
log.info("授权方法检索权限:当前的用户="+user);
String userid = (String) user.get("id");
String username = (String) user.get("username");
//权限集合
Set permsSet=new HashSet<>();
//根据用户的信息查权限存入set
if("123".equals(userid)){
permsSet.add("sys:user");
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(permsSet);
return info;
}
/**
* 认证,登录的时候会调用
* 调用时机
* Subject subject = SecurityUtils.getSubject();
* subject.login(token);
* 在自定义token过滤器的executeLogin方法也会调用到上面两行,所以也会执行到下面的doGetAuthenticationInfo,每次请求都会认证
* authenticationToken能强转成字符串,因为用的是自定义的String类型的token
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String token = (String) authenticationToken.getPrincipal();
//通过token到redis缓存里获取到对应用户user
Map userEntity=new HashMap<>();
if(jwtOperator.validateToken(token)){
Claims claims = jwtOperator.getClaimsFromToken(token);
userEntity.put("id",claims.get("id"));
userEntity.put("username",claims.get("username"));
log.info("认证token");
//将 token id username 存入上下文
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = attributes.getRequest();
request.setAttribute("token",token);
request.setAttribute("id",claims.get("id"));
request.setAttribute("username",claims.get("username"));
}else{
log.warn("token失效");
throw new IncorrectCredentialsException("token失效");
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userEntity, token, getName());
return info;
}
}
自定义token
package com.vvvtimes.demo.auth;
import org.apache.shiro.authc.AuthenticationToken;
/**
* 自定义token,在自定义token过滤器里使用
*/
public class ShiroAuthToken implements AuthenticationToken {
private String token;
public ShiroAuthToken(String token){
this.token=token;
}
@Override
public String getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
登录方法,获取token和用户信息
/**
* 登录操作
*
* @return
*/
@RequestMapping(value = "/doLogin", method = {RequestMethod.GET, RequestMethod.POST})
public RestResponse doLogin(@RequestBody LoginData entity) {
RestResponse restResponse = new RestResponse<>();
LoginData loginData = entity;
String username = loginData.getUsername();
String password = loginData.getPassword();
Boolean rememberMe = loginData.getRememberMe();
User loginUser = userService.findUserByName(username);
if (loginUser == null) {
restResponse.setStatus(CommonConstants.USER_NOT_EXIST);
return restResponse;
}
//盐
String salt = loginUser.getSalt();
String dbPassword = loginUser.getPassword();
String encryptedText = DigestUtil.md5Hex(password + salt);
if (dbPassword.equals(encryptedText)) {
Map userInfoClaims = new HashMap<>();
userInfoClaims.put("id", loginUser.getId().toString());
userInfoClaims.put("username", loginUser.getUsername());
userInfoClaims.put("role", "user");
userInfoClaims.put("avatarUrl", loginUser.getAvatarUrl());
String token = jwtOperator.generateToken(userInfoClaims);
//String token = JWTUtil.sign(username, password, loginUser.getUuid(), rememberMe);
UserInfo userInfo = new UserInfo();
userInfo.setToken(token);
userInfo.setUser(loginUser);
restResponse.setResult(userInfo);
} else {
restResponse.setStatus(CommonConstants.USER_LOGIN_FILED);
return restResponse;
}
return restResponse;
}
查询方法,查询用户列表
//controller层
/**
* 查询人员列表
*
*
* @return
* @throws NoSuchFieldException
*/
@RequestMapping(value = "/getUserList", method = {RequestMethod.POST, RequestMethod.GET})
public @ResponseBody
RestResponse> getUserList(@RequestBody BasePageRequest entity) {
return service.getUserList(entity);
}
//service层
public RestResponse> getUserList(BasePageRequest entity) {
RestResponse> result = new RestResponse<>();
UserQueryVo queryVo = entity.getEntity();
PageHelper.startPage(entity.getPageNum(), entity.getPageSize());
List userVoList = userMapper.getUserList(queryVo);
result.setResult(new PageInfo<>(userVoList));
return result;
}
//mapper层
List getUserList(UserQueryVo queryVo);
//mapper xml层
验证登录接口
curl --location 'http://localhost:9080/login/doLogin' \
--header 'Content-Type: application/json' \
--data '{
"username":"admin",
"password":"admin"
}'
验证查询列表接口
curl --location 'http://localhost:9080/user/getUserList' \
--header 'token: adc799d0341a4c84ab5c77c3504d1328' \
--header 'Content-Type: application/json' \
--data '{
"username":"user1",
"password":"pass1"
}'
io.jsonwebtoken
jjwt-api
0.10.7
io.jsonwebtoken
jjwt-impl
0.10.7
runtime
io.jsonwebtoken
jjwt-jackson
0.10.7
runtime
org.springframework.boot
spring-boot-starter-security
config类
package com.vvvtimes.demo.auth.config;
import com.vvvtimes.demo.auth.TokenAuthenticateFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@EnableWebSecurity
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
// 设置 session 为无状态,因为基于 token 不需要 session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.sessionFixation().none()
.and()
.authorizeRequests()
.antMatchers("/login/**").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable()
.addFilterBefore(new TokenAuthenticateFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/login/**");
}
}
token过滤器类
package com.vvvtimes.demo.auth;
import cn.hutool.json.JSONUtil;
import com.vvvtimes.demo.common.dto.RestResponse;
import com.vvvtimes.demo.util.JwtOperator;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class TokenAuthenticateFilter extends OncePerRequestFilter {
private JwtOperator jwtOperator;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取用户token
String token = getToken(request);
try{
//jwt解析token
if(jwtOperator.validateToken(token)){
Claims claims = jwtOperator.getClaimsFromToken(token);
log.info("认证token");
//将 token id username 存入上下文
request.setAttribute("token",token);
request.setAttribute("id",claims.get("id"));
request.setAttribute("username",claims.get("username"));
}else{
log.warn("token失效");
writeFailureResponse(response,request);
return;
}
}catch (NullPointerException exception){
log.warn("token异常NullPointerException");
writeFailureResponse(response,request);
return;
}
filterChain.doFilter(request, response);
}
private void writeFailureResponse(HttpServletResponse response,HttpServletRequest request) throws IOException {
response.setContentType("application/json;charset=utf-8");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.setCharacterEncoding("utf-8");
response.setHeader("ContentType","application/json;charset=utf-8");
response.setContentType("application/json;charset=utf-8");
RestResponse restResponse = new RestResponse<>();
restResponse.setStatus(401);
restResponse.setMessage("token校验失败");
String json = JSONUtil.toJsonStr(restResponse);
response.getWriter().print(json);
}
/**
* 方法获取用户的token
* @param request
* @return token
*/
public String getToken(HttpServletRequest request){
//获取请求头
String token = request.getHeader("token");
//没有请求头就从参数里拿
if (!StringUtils.hasText(token)){
token=request.getParameter("token");
}
return token;
}
}
其他代码 工具类,登录 查列表 跟shrio实现一样,不赘述
跟shrio一样
io.jsonwebtoken
jjwt-api
0.10.7
io.jsonwebtoken
jjwt-impl
0.10.7
runtime
io.jsonwebtoken
jjwt-jackson
0.10.7
runtime
org.springframework.boot
spring-boot-starter-aop
定义注解
package com.vvvtimes.demo.auth;
public @interface CheckLogin {
}
实现注解
package com.vvvtimes.demo.auth;
import com.vvvtimes.demo.auth.exception.AOPSecurityException;
import com.vvvtimes.demo.util.JwtOperator;
import io.jsonwebtoken.Claims;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
@Aspect
@Component
public class CheckLoginAspect {
@Autowired
private JwtOperator jwtOperator;
@Around("@annotation(com.vvvtimes.demo.auth.CheckLogin)")
public Object checkLogin(ProceedingJoinPoint point) {
try {
// 1.从header里面获取token
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader("token");
// 2.校验token是否合法&是否过期,如果不合法或已过期 直接抛出异常,如果合法放行
Boolean isValid = jwtOperator.validateToken(token);
if (!isValid ) {
throw new AOPSecurityException("token不合法");
}
//3如果校验成功,那么将客户信息 设置到 request的attribute里面
Claims claims = jwtOperator.getClaimsFromToken(token);
request.setAttribute("id",claims.get("id"));
request.setAttribute("username",claims.get("username"));
request.setAttribute("role",claims.get("role"));
request.setAttribute("avatarUrl",claims.get("avatarUrl"));
return point.proceed();
} catch (Throwable throwable) {
throw new AOPSecurityException("token不合法");
}
}
}
处理异常
package com.vvvtimes.demo.auth;
import cn.hutool.core.lang.copier.Copier;
import com.vvvtimes.demo.auth.exception.AOPSecurityException;
import com.vvvtimes.demo.common.dto.BaseResponse;
import com.vvvtimes.demo.common.dto.RestResponse;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionErrorHanlder {
@ExceptionHandler(AOPSecurityException.class)
public ResponseEntity> error(AOPSecurityException e) {
log.warn("发生AOPSecurityException异常",e);
RestResponse restResponse = new RestResponse<>();
restResponse.setStatus(401);
restResponse.setMessage("token校验失败");
ResponseEntity> response = new ResponseEntity>(
restResponse,
HttpStatus.UNAUTHORIZED
);
return response;
}
}
自定义异常类
package com.vvvtimes.demo.auth.exception;
public class AOPSecurityException extends RuntimeException {
public AOPSecurityException(String string) {
super(string);
}
}
使用时注意在需要token的方法上加上CheckLogin注解
如
/**
* 查询人员列表
*
*
* @return
* @throws NoSuchFieldException
*/
@CheckLogin
@RequestMapping(value = "/getUserList", method = {RequestMethod.POST, RequestMethod.GET})
public @ResponseBody
RestResponse> getUserList(@RequestBody BasePageRequest entity) {
return service.getUserList(entity);
}
其他工具类 登录 查列表代码与shrio方式一样
跟shrio方式一样
相比较来说,redis只需要参照实现前面的jwt工具类就行了,这里做了个简单的限制,限制单用户只能生成10个不同的token,如果多生成,则注销最早的token
org.springframework.boot
spring-boot-starter-aop
org.springframework.boot
spring-boot-starter-data-redis
redis.clients
jedis
2.9.3
spring:
redis:
host: localhost
port: 6379
这里配置redis密码,生产环境强烈配置密码,见过太多redis入侵了
redis工具类
package com.vvvtimes.demo.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
@Autowired
private RedisTemplate redisTemplate;
@Autowired(required = false)
public void setRedisTemplate(RedisTemplate redisTemplate) {
RedisSerializer stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setValueSerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
redisTemplate.setHashValueSerializer(stringSerializer);
this.redisTemplate = redisTemplate;
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map
redistoken工具类
package com.vvvtimes.demo.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Component
public class RedisTokenOperator {
/**
* 有效期,单位秒
* - 默认2周
*/
@Value("${redistoken.expire-time-in-second:1209600}")
private Long expirationTimeInSecond;
public static final String TOKEN_PREFIX_KEY = "token:";
public static final String USER_PREFIX_KEY = "user:";
public static final Integer SINGLE_USER_MAX_TOKEN_SIZE = 10;
/*@Autowired
private RedisTemplate redisTemplate;*/
@Autowired
private RedisUtil redisUtil;
/*
* 从token中获取UserMap
*/
public Map
定义注解
package com.vvvtimes.demo.auth;
public @interface CheckLogin {
}
实现注解
package com.vvvtimes.demo.auth;
import com.vvvtimes.demo.auth.exception.AOPSecurityException;
import com.vvvtimes.demo.util.RedisTokenOperator;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
@Aspect
@Component
public class CheckLoginAspect {
@Autowired
private RedisTokenOperator redisTokenOperator;
@Around("@annotation(com.vvvtimes.demo.auth.CheckLogin)")
public Object checkLogin(ProceedingJoinPoint point) {
try {
// 1.从header里面获取token
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader("token");
// 2.校验token是否合法&是否过期,如果不合法或已过期 直接抛出异常,如果合法放行
Boolean isValid = redisTokenOperator.validateToken(token);
if (!isValid ) {
throw new AOPSecurityException("token不合法");
}
//3如果校验成功,那么将客户信息 设置到 request的attribute里面
Map claims = redisTokenOperator.getUserMapFromToken(token);
request.setAttribute("id",claims.get("id"));
request.setAttribute("username",claims.get("username"));
request.setAttribute("role",claims.get("role"));
request.setAttribute("avatarUrl",claims.get("avatarUrl"));
return point.proceed();
} catch (Throwable throwable) {
throw new AOPSecurityException("token不合法");
}
}
}
处理异常
package com.vvvtimes.demo.auth;
import cn.hutool.core.lang.copier.Copier;
import com.vvvtimes.demo.auth.exception.AOPSecurityException;
import com.vvvtimes.demo.common.dto.BaseResponse;
import com.vvvtimes.demo.common.dto.RestResponse;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionErrorHanlder {
@ExceptionHandler(AOPSecurityException.class)
public ResponseEntity> error(AOPSecurityException e) {
log.warn("发生AOPSecurityException异常",e);
RestResponse restResponse = new RestResponse<>();
restResponse.setStatus(401);
restResponse.setMessage("token校验失败");
ResponseEntity> response = new ResponseEntity>(
restResponse,
HttpStatus.UNAUTHORIZED
);
return response;
}
}
定义异常
package com.vvvtimes.demo.auth.exception;
public class AOPSecurityException extends RuntimeException {
public AOPSecurityException(String string) {
super(string);
}
}
登录方法没有Claim类了,一并更改
/**
* 登录操作
*
* @return
*/
@RequestMapping(value = "/doLogin", method = {RequestMethod.GET, RequestMethod.POST})
public RestResponse doLogin(@RequestBody LoginData entity) {
RestResponse restResponse = new RestResponse<>();
LoginData loginData = entity;
String username = loginData.getUsername();
String password = loginData.getPassword();
Boolean rememberMe = loginData.getRememberMe();
User loginUser = userService.findUserByName(username);
if (loginUser == null) {
restResponse.setStatus(CommonConstants.USER_NOT_EXIST);
return restResponse;
}
//盐
String salt = loginUser.getSalt();
String dbPassword = loginUser.getPassword();
String encryptedText = DigestUtil.md5Hex(password + salt);
if (dbPassword.equals(encryptedText)) {
Map userInfoClaims = new HashMap<>();
userInfoClaims.put("id", loginUser.getId().toString());
userInfoClaims.put("username", loginUser.getUsername());
userInfoClaims.put("role", "user");
userInfoClaims.put("avatarUrl", loginUser.getAvatarUrl());
String token = redisTokenOperator.generateToken(userInfoClaims);
//String token = JWTUtil.sign(username, password, loginUser.getUuid(), rememberMe);
UserInfo userInfo = new UserInfo();
userInfo.setToken(token);
userInfo.setUser(loginUser);
restResponse.setResult(userInfo);
} else {
restResponse.setStatus(CommonConstants.USER_LOGIN_FILED);
return restResponse;
}
return restResponse;
}
其他代码跟shrio没有区别
跟shrio一致。
redis里看到的token如下