IntelliJ+SpringBoot项目实战(十八)--在SpringBoot中整合SpringSecurity和JWT(下C)
九、实现JWT认证过滤器
在上节中实现了SpringSecurity的权限控制,本节介绍JWT登录相关的实现。
9.1 实现JwtAuthenticationFilter
首先实现JwtAuthenticationFilter,此过滤器的作用是,检查头部的Authorization中是否有
accessToken,如果没有,按照传统的登录方式进行,如果有accessToken,则从accessToken中解析出登录账号,然后通过CommUser sysUser = sysUserService.selectUserByLoginId(username);从数据库中读取用户信息,并封装到SpringSecurity上下文中实现自动登录:
UsernamePasswordAuthenticationToken token = new
UsernamePasswordAuthenticationToken(username, null,
userDetailService.getUserAuthority(sysUser.getLoginId()));
SecurityContextHolder.getContext().setAuthentication(token);
chain.doFilter(request, response);
下面是JwtAuthenticationFilter完整代码:
package org.openjweb.sys.filter;
import cn.hutool.core.util.StrUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import lombok.extern.slf4j.Slf4j;
import org.openjweb.core.entity.CommUser;
import org.openjweb.core.service.CommUserService;
import org.openjweb.core.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
JwtUtil jwtUtils;
@Autowired
CommUserService userDetailService;
@Autowired
CommUserService sysUserService;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String jwt = null;
if(jwtUtils==null){
log.info("jwtUtils is null。。。。。。。。。。");
}
else{
log.info("jwt utils is not null,jwtutils.getHeader:");//非空
log.info(jwtUtils.getHeader());//空
String header = jwtUtils.getHeader();//
if(header!=null) {
log.info("jwt header:::::"+header );
jwt = request.getHeader(header);
}
}
// 这里如果没有jwt,继续往后走,因为后面还有鉴权管理器等去判断是否拥有身份凭证,所以是可以放行的
// 没有jwt相当于匿名访问,若有一些接口是需要权限的,则不能访问这些接口
if (StrUtil.isBlankOrUndefined(jwt)) {
log.info("没有jwt信息,继续filter........");
chain.doFilter(request, response);
return;
}
else{
log.info("有jwt..........");
}
Claims claim = jwtUtils.getClaimsByToken(jwt);
if (claim == null) {
throw new JwtException("token 异常");
}
if (jwtUtils.isTokenExpired(claim)) {
throw new JwtException("token 已过期");
}
String username = claim.getSubject();
// 获取用户的权限等信息
CommUser sysUser = sysUserService.selectUserByLoginId(username);
// 构建UsernamePasswordAuthenticationToken,这里密码为null,是因为提供了正确的JWT,实现自动登录
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, userDetailService.getUserAuthority(sysUser.getLoginId()));
SecurityContextHolder.getContext().setAuthentication(token);
chain.doFilter(request, response);
}
}
然后在WebSecurity.java中配置此过滤器:
@Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
return jwtAuthenticationFilter;
}
然后需要将这个过滤器已加到配置里:
不过实际测试时,没有增加上面红框的代码,过滤器也生效了,可能这样加可以对过滤器的执行顺序进行排序。
然后我们启动SpringBoot,仍访问http://localhost:8001/auth/test。控制台显示:
说明过滤器已经被调用。
十、登录成功处理
接下来增加一个登录成功处理类,当登录成功后,将生成的accesstoken设置到header中:
package org.openjweb.sys.handler;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.openjweb.common.response.ResponseResult;
import org.openjweb.core.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Component
@Slf4j
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
JwtUtil jwtUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
log.info("success ,add jwt accesstoken to header.........");
// 生成JWT,并放置到请求头中
String jwt = jwtUtils.generateToken(authentication.getName());
log.info("jwtUtils.getHeader():");
log.info(jwtUtils.getHeader());
log.info(jwt);
httpServletResponse.setHeader(jwtUtils.getHeader(), jwt);
ResponseResult result = ResponseResult.okResult("SuccessLogin");//登录成功
String json = JSONUtil.toJsonStr(result);
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
然后在WebSecurityConfig.java中,增加这个Handler:
@Autowired
LoginSuccessHandler loginSuccessHandler;
启动SpringBoot,还是访问localhost:8001/api/auth/test,登录成功的界面:
在控制台中可以看到生成的accessToken:
十、登录失败处理
增加一个登录失败处理类LoginFailureHandler:
package org.openjweb.sys.handler;
import cn.hutool.json.JSONUtil;
import org.openjweb.common.response.ResponseResult;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
String errorMessage = "用户名或密码错误";
ResponseResult result = ResponseResult.errorResult(-1,errorMessage);
String json = JSONUtil.toJsonStr(result);
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
然后在WebSecureConfig中增加组件:
@Autowired
LoginSuccessHandler loginSuccessHandler;
启动SpringBoot,访问localhost:8001/api/auth/test,输入错误的密码,返回的界面:
十一、异常处理
当未登录的时候访问受限资源或权限不足,可进行下面的处理。未登录访问受限资源,使用JwtAuthenticationEntryPoint来处理,当权限不足时,使用JwtAccessDeniedHandler处理,下面是实现代码:
package org.openjweb.sys.handler;
import cn.hutool.json.JSONUtil;
import org.openjweb.common.response.ResponseResult;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
//错误码后面在常量类中补充
ResponseResult result = ResponseResult.errorResult(-2,"权限不足");//权限不足
String json = JSONUtil.toJsonStr(result);
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
package org.openjweb.sys.entry;
import cn.hutool.json.JSONUtil;
import org.openjweb.common.response.ResponseResult;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
//认证失败的处理
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
ResponseResult result = ResponseResult.errorResult(-3,"请先登录");//权限不足
String json = JSONUtil.toJsonStr(result);
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
在WebSecurityConfig中配置:
@Autowired
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
JwtAccessDeniedHandler jwtAccessDeniedHandler;
增加异常处理:
十二、退出登录成功处理
退出登录成功处理使用JWTLogoutSuccessHandler类:
package org.openjweb.sys.handler;
import cn.hutool.json.JSONUtil;
import org.openjweb.common.response.ResponseResult;
import org.openjweb.core.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Component
public class JWTLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
JwtUtil jwtUtils;
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
Authentication authentication) throws IOException, ServletException {
if (authentication != null) {
new SecurityContextLogoutHandler().logout(httpServletRequest, httpServletResponse, authentication);
}
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
httpServletResponse.setHeader(jwtUtils.getHeader(), "");
ResponseResult result = ResponseResult.okResult("注销成功!");
String json = JSONUtil.toJsonStr(result);
outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
在WebSecurityConfig中配置:
@Autowired
JWTLogoutSuccessHandler jwtLogoutSuccessHandler;
顺便增加了session禁用的配置。注销成功处理,注销成功可以访问http://localhost:8001/logout
界面显示:{"msg":"操作成功","code":0,"data":"注销成功!"}
注:实测登录时,如果增加了.authenticationEntryPoint(jwtAuthenticationEntryPoint)会导致下次登录不能访问/login页面,所以应去掉这行代码。
写到这里,SpringSecurity和JWT的整合已经完全实现了。但是还有一个问题,SpringSecurity的登录界面自定义?而且在前后端分离的项目中,登录是VUE端通过接口调用登录的,而不是走/login界面的登录。下一篇文章介绍SpringBoot的前后端分离模式的登录方式。
本文完整代码见Github: GitHub - openjweb/cloud at masterOpenJWeb is a java bases low code platform. Contribute to openjweb/cloud development by creating an account on GitHub.https://github.com/openjweb/cloud/tree/master