spring security+jwt+redis实现无状态服务安全框架(1)
首先,我们知道springsecurity基于会话进行安全管理,我们要实现无状态的安全框架就不能基于会话。所以加入jwt,spring security我们用它只是用它所拥有的各种过滤器来完成我们的功能。
1.依赖导入
使用springboot进行开发:
2.过滤器
过滤器在项目中是很重要的,尤其是安全框架,我们看看spring security的过滤器顺序,后续我们会根据过滤器开发。
3.思路
在认证过滤器之前,我们自定义两个过滤器
1.
JwtLoginFilter
用来拦截登录请求进行成功与失败处理,继承
UsernamePasswordAuthenticationFilte
过滤器(当然登录成功失败处理可以自定义,加此过滤器可以做一下额外处理)
2.
JwtAuthenticationFilte
拦截请求并判断token,继承
BasicAuthenticationFilter
此过滤器针对的是拥有Authorization头部信息的请求 咋们的token就在此存储,继承此过滤器对token进行处理
JwtLoginFilter
package com.guidezhilv.guideconsolewebboot.security;
import java.io.*;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.guidezhilv.guideconsolewebboot.model.User;
import com.guidezhilv.guideconsolewebboot.security.loginFail.MyAuthenticationFailureHandler;
import com.guidezhilv.guideconsolewebboot.security.loginSuccess.MyAuthenticationSuccessHandler;
import com.guidezhilv.guideconsolewebboot.service.UserService;
import com.guidezhilv.guideconsolewebboot.service.impl.SysUserServiceImpl;
import com.guidezhilv.guideconsolewebboot.utils.JwtTokenUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.stereotype.Component;
import org.springframework.web.context.support.WebApplicationContextUtils;
/**
* 启动登录认证流程过滤器
* @author Louis
* @date Jun 29, 2019
*/
@Component
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
public JwtLoginFilter(AuthenticationManager authManager) {
setAuthenticationManager(authManager);
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
// POST 请求 /login 登录时拦截, 由此方法触发执行登录认证流程,可以在此覆写整个登录认证逻辑
super.doFilter(req, res, chain);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 可以在此覆写尝试进行登录认证的逻辑,登录成功之后等操作不再此方法内
// 如果使用此过滤器来触发登录认证流程,注意登录请求数据格式的问题
// 此过滤器的用户名密码默认从request.getParameter()获取,但是这种
// 读取方式不能读取到如 application/json 等 post 请求数据,需要把
// 用户名密码的读取逻辑修改为到流中读取request.getInputStream()
String body = getBody(request);
JSONObject jsonObject = JSON.parseObject(body);
String username = jsonObject.getString("username");
String password = jsonObject.getString("password");
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
JwtAuthenticatioToken authRequest = new JwtAuthenticatioToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
@Override
public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// 存储登录认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authResult);
// 记住我服务
// getRememberMeServices().loginSuccess(request, response, authResult);
// 触发事件监听器
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
// 生成并返回token给客户端,后续访问携带此token
Object principal = authResult.getPrincipal();
String username = ((UserDetails) principal).getUsername();
SysUserServiceImpl userDetailsService=new SysUserServiceImpl();
User byUsername = userDetailsService.findByUsername(username);
JwtAuthenticatioToken token = new JwtAuthenticatioToken(null, null, JwtTokenUtils.generateToken(authResult,byUsername.getId()+""));
token.setUserKey(byUsername.getId());
MyAuthenticationSuccessHandler myAuthenticationSuccessHandler = new MyAuthenticationSuccessHandler();
myAuthenticationSuccessHandler.onAuthenticationSuccess(request,response,token);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
MyAuthenticationFailureHandler myAuthenticationFailureHandler = new MyAuthenticationFailureHandler();
myAuthenticationFailureHandler.onAuthenticationFailure(request,response,failed);
}
/**
* 获取请求Body
* @param request
* @return
*/
public String getBody(HttpServletRequest request) {
StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = request.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line = "";
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return sb.toString();
}
}
JwtAuthenticationFilte
package com.guidezhilv.guideconsolewebboot.security;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.guidezhilv.guideconsolewebboot.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
/**
* 登录认证检查过滤器
* @author Louis
* @date Jun 29, 2019
*/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 获取token, 并检查登录状态 调用工具类判断token
boolean state = SecurityUtils.checkAuthentication(request);
if (!state){
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println("{\"state\":207,\"msg\":\"token过期重新登录\"}");
response.getWriter().flush();
}else{
chain.doFilter(request, response);
}
}
}
登录成功与失败处理
在前后端分离中,指定成功失败页面耦合性太强,所以登录成功失败处理 直接返回json 数据。况且在jwt中为了保证token的安全性,登录成功会将token存入redis,以保证无状态。
在上文提到
JwtLoginFilter
过滤,重写方法中有失败和成功的处理方法
successfulAuthentication
,
unsuccessfulAuthentication
通过重写这两个方法可以返回指定数据。
@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private static RedisTemplate redisTemplate;
@Autowired
public void setRedisTemplate(RedisTemplate redisTemplate){
MyAuthenticationSuccessHandler.redisTemplate=redisTemplate;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws ServletException, IOException {
//将用户id和token存入redis
if (authentication instanceof JwtAuthenticatioToken){
JwtAuthenticatioToken authentication1 = (JwtAuthenticatioToken) authentication;
ValueOperations valueOperations = redisTemplate.opsForValue();
valueOperations.set(authentication1.getUserKey()+"",authentication1.getToken(),60*60*2, TimeUnit.SECONDS);
}
//例1:不跳到XML设定的页面,而是直接返回json字符串
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
JwtAuthenticatioToken authentication1 = (JwtAuthenticatioToken) authentication;
response.getWriter().println("{\"state\":\"200\",\" msg\":\"登录成功\",\" token\":"+authentication1.getToken()+"}");
}
};
MyAuthenticationFailureHandler
public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
Example1(request, response, exception);
// Example2(request,response,exception);
// Example3(request,response,exception);
}
private void Example1(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
System.out.println(exception.getClass());
if (exception instanceof BadCredentialsException) {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println("{\"state\":205,\"msg\":\"密码错误\"}");
} else if (exception instanceof AuthenticationServiceException) {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println("{\"state\":206,\"msg\":\"没有此用户\"}");
} else {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println("{\"state\":204,\"msg\":\"登录失败\"}");
}
//例1:直接返回字符串
}
private void Example2(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String strUrl = request.getContextPath() + "/customLoginResponse.jsp";
request.getSession().setAttribute("ok", 0);
request.getSession().setAttribute("message", exception.getLocalizedMessage());
request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
super.onAuthenticationFailure(request, response, exception);
}
private void Example3(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
//例3:自定义跳转到哪个URL
//假设login.jsp在webapp路径下
//注意:不能访问WEB-INF下的jsp。
String strUrl = request.getContextPath() + "/customLoginResponse.jsp";
request.getSession().setAttribute("ok", 0);
request.getSession().setAttribute("message", exception.getLocalizedMessage());
request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
//Error request.getRequestDispatcher(strUrl).forward(request, response);
response.sendRedirect(strUrl);
}
};
授权工具类
在
JwtAuthenticationFilter
中用来校验token的状态 比如 token过期,token是否正确。
SecurityUtils
public class SecurityUtils {
/**
* 系统登录认证
* @param request
* @param username
* @param password
* @param authenticationManager
* @return
*/
public static JwtAuthenticatioToken login(HttpServletRequest request, String username, String password, AuthenticationManager authenticationManager) {
JwtAuthenticatioToken token = new JwtAuthenticatioToken(username, password);
token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 执行登录认证过程
Authentication authentication = authenticationManager.authenticate(token);
// 认证成功存储认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成令牌并返回给客户端
// token.setToken(JwtTokenUtils.generateToken(authentication));
return token;
}
/**
* 获取令牌进行认证
* @param request
*/
public static boolean checkAuthentication(HttpServletRequest request) throws IOException {
// 获取令牌并根据令牌获取登录认证信息
Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request);
// 设置登录认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
if (authentication==null){
return false;
}
return true;
}
/**
* 获取当前用户名
* @return
*/
public static String getUsername() {
String username = null;
Authentication authentication = getAuthentication();
if(authentication != null) {
Object principal = authentication.getPrincipal();
if(principal != null && principal instanceof UserDetails) {
username = ((UserDetails) principal).getUsername();
}
}
return username;
}
/**
* 获取用户名
* @return
*/
public static String getUsername(Authentication authentication) {
String username = null;
if(authentication != null) {
Object principal = authentication.getPrincipal();
if(principal != null && principal instanceof UserDetails) {
username = ((UserDetails) principal).getUsername();
}
}
return username;
}
/**
* 获取当前登录信息
* @return
*/
public static Authentication getAuthentication() {
if(SecurityContextHolder.getContext() == null) {
return null;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication;
}
}
JwtTokenUtils
public class JwtTokenUtils implements Serializable {
private static final long serialVersionUID = 1L;
private static RedisTemplate redisTemplate;
@Autowired
public void setRedisTemplate(RedisTemplate redisTemplate){
JwtTokenUtils.redisTemplate=redisTemplate;
}
/**
* 用户名称
*/
private static final String USERNAME = Claims.SUBJECT;
/**
* 创建时间
*/
private static final String CREATED = "created";
/**
* 权限列表
*/
private static final String AUTHORITIES = "authorities";
/**
* 用户id
*/
private static final String USERKEY = "userkey";
/**
* 密钥
*/
private static final String SECRET = "abcdefgh";
/**
* 有效期12小时 12 * 60 *60 * 1000
*/
private static final long EXPIRE_TIME = 12*60*60* 1000;
/**
* 生成令牌
*
* @param
* @return 令牌
*/
public static String generateToken(Authentication authentication, String id) {
Map<String, Object> claims = new HashMap<>(3);
claims.put(USERNAME, SecurityUtils.getUsername(authentication));
claims.put(CREATED, new Date());
claims.put(USERKEY, id);
claims.put(AUTHORITIES, authentication.getAuthorities());
return generateToken(claims);
}
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private static String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact();
}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
public static String getUsernameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 根据请求令牌获取登录认证信息
*
* @param
* @return 用户名
*/
public static Authentication getAuthenticationeFromToken(HttpServletRequest request) {
Authentication authentication = null;
// 获取请求携带的令牌
String token = JwtTokenUtils.getToken(request);
if (token != null) {
// 请求令牌不能为空
if (SecurityUtils.getAuthentication() == null) {
if (isTokenExpired(token)) {
return null;
}
if (!valRedisToken(token, getUserKeyByToken(token)+"")) {
return null;
}
// 上下文中Authentication为空
Claims claims = getClaimsFromToken(token);
if (claims == null) {
return null;
}
String username = claims.getSubject();
if (username == null) {
return null;
}
Object authors = claims.get(AUTHORITIES);
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
if (authors != null && authors instanceof List) {
for (Object object : (List) authors) {
authorities.add(new GrantedAuthorityImpl((String) ((Map) object).get("authority")));
}
}
authentication = new JwtAuthenticatioToken(new JwtUserDetails(username, "", authorities), null, authorities, token);
} else {
if (validateToken(token, SecurityUtils.getUsername())) {
// 如果上下文中Authentication非空,且请求令牌合法,直接返回当前登录认证信息
authentication = SecurityUtils.getAuthentication();
}
}
}
return authentication;
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private static Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 验证令牌
*
* @param token
* @param username
* @return
*/
public static Boolean validateToken(String token, String username) {
String userName = getUsernameFromToken(token);
if (valRedisToken(token, getUserKeyByToken(token)+"")) {
if (StringUtils.isEmpty(userName)) {
return false;
}
return (userName.equals(username) && !isTokenExpired(token));
}
return false;
}
/*
* 用token获取用户key
* */
public static Long getUserKeyByToken(String token) {
Claims claimsFromToken = getClaimsFromToken(token);
Long userKey = Long.parseLong( claimsFromToken.get(USERKEY)+"");
return userKey;
}
/*
* 判断redis中是否有token及是否过期
* */
public static Boolean valRedisToken(String token, String userKey) {
//查询redis中的token
if (redisTemplate.hasKey(userKey)) {
ValueOperations valueOperations = redisTemplate.opsForValue();
String redisToken = (String) valueOperations.get(userKey);
if (StringUtils.isNotBlank(redisToken) && token.equals(redisToken)) {
return true;
}
}
return false;
}
/**
* 刷新令牌
*
* @param token
* @return
*/
public static String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put(CREATED, new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
/**
* 判断令牌是否过期
*
* @param token 令牌
* @return 是否过期
*/
public static Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
boolean before = expiration.before(new Date());
return before;
} catch (Exception e) {
return true;
}
}
/**
* 获取请求token
*
* @param request
* @return
*/
public static String getToken(HttpServletRequest request) {
String token = request.getHeader("Authorization");
String tokenHead = "Bearer ";
if (token == null) {
token = request.getHeader("token");
} else if (token.contains(tokenHead)) {
token = token.substring(tokenHead.length());
}
if ("".equals(token)) {
token = null;
}
return token;
}
}
对于认证登录信息,使用的安全框架默认的认证方法,你可以实现UserDetailsService重写loadUserByUsername方法来完成登录信息与数据库信息认证
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("该用户不存在");
}
// 用户权限列表,根据用户拥有的权限标识与如 @PreAuthorize("hasAuthority('sys:menu:view')") 标注的接口对比,决定是否可以调用接口
Set<String> permissions = userService.findPermissions(username);
List<GrantedAuthority> grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList());
return new JwtUserDetails(username, user.getPassword(), grantedAuthorities);
}
}
4.结尾
此篇文章只是大概说明使用方法,详情代码
github代码详情