目录
前言
依赖引入:
JWT工具类
修改CustomizeAuthenticationSuccessHandler代码
修改登录失败处理器CustomizeAuthenticationFailureHandler
redis工具类和验证码配置
编写获取验证码的接口:
验证码过滤器CaptchaFilter:
JWT过滤器JwtAuthenticationFilter
jwt认证失败处理器
无权限访问处理
Spring Security全局配置:WebSecurityConfig
测试:
本篇文章是基于上一篇文章进行的整理扩展,没有看过的可以看一下上一篇文章
SpringBoot整合Spring Security实现前后端分离登录权限处理_zmgst的博客-CSDN博客
本篇文章的思路是基于这位博主的博客进行的开发,一些对于jwt的描述,session和token的不同描述的很不错,原文地址:
【全网最细致】SpringBoot整合Spring Security + JWT实现用户认证_小灵宝的博客-CSDN博客_springboot整合security+jwt
由于我也使用了验证码的,所以引入了redis,因为我的jdk11版本,所以需要引入一些依赖,如果jdk1.8就不需要那些依赖,可以自行删除
org.springframework.boot
spring-boot-starter-data-redis
io.jsonwebtoken
jjwt
0.9.0
com.github.axet
kaptcha
0.0.9
javax.xml.bind
jaxb-api
2.3.0
com.sun.xml.bind
jaxb-impl
2.3.0
com.sun.xml.bind
jaxb-core
2.3.0
javax.activation
activation
1.1.1
cn.hutool
hutool-all
5.3.3
org.apache.commons
commons-lang3
3.8.1
commons-codec
commons-codec
1.15
org.springframework.boot
spring-boot-configuration-processor
true
首先写一个JWT工具类JwtUtils,该工具类需要有3个功能:生成JWT、解析JWT、判断JWT是否过期。在此我们使用了@ConfigurationProperties注解,方便读取application.yml文件里的内容。
/**
* @Author: zm
* @Description: jwt工具类
* @Date: 2022/4/24 16:48
*/
@Data
@Component
@ConfigurationProperties(prefix = "zm.jwt")
public class JwtUtils {
private long expire;
private String secret;
private String header;
// 生成JWT
public String generateToken(String username) {
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + 1000 * expire);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(username)
.setIssuedAt(nowDate)
.setExpiration(expireDate) // 7天过期
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
// 解析JWT
public Claims getClaimsByToken(String jwt) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(jwt)
.getBody();
} catch (Exception e) {
return null;
}
}
// 判断JWT是否过期
public boolean isTokenExpired(Claims claims) {
return claims.getExpiration().before(new Date());
}
/**
* 根据token,判断token是否存在与有效
* @param jwtToken
* @return
*/
public boolean checkToken(String jwtToken) {
if(StringUtils.isEmpty(jwtToken)) return false;
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 根据request判断token是否存在与有效(也就是把token取出来罢了)
* @param request
* @return
*/
public boolean checkToken(HttpServletRequest request) {
try {
String jwtToken = request.getHeader(header);
if(StringUtils.isEmpty(jwtToken)) return false;
Jwts.parser().setSigningKey(secret).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 根据token获取用户的account
* @param request
* @return
*/
public String getMemberAccountByJwtToken(HttpServletRequest request) {
String jwtToken = request.getHeader(header);
if(StringUtils.isEmpty(jwtToken)) return "";
try{
Jws claimsJws = Jwts.parser().setSigningKey(secret).parseClaimsJws(jwtToken);
Claims claims = claimsJws.getBody();
return claims.getSubject();
}catch (Exception e){
e.printStackTrace();
return "";
}
}
}
对应的配置文件:
zm: jwt: header: Authorization expire: 604800 # 7天,s为单位 secret: abcdefghabcdefghabcdefghabcdefgh
代码:
/**
* @Author: zm
* @Description:登录成功处理逻辑
* @Date: 2022/4/24 10:18
*/
@Component
public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private SysUserService sysUserService;
@Autowired
JwtUtils jwtUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//更新用户表上次登录时间、更新人、更新时间等字段
User userDetails = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
SysUser sysUser = sysUserService.getUserDetails(userDetails.getUsername());
// sysUser.setLastLoginTime(new Date());
sysUser.setUpdateDate(LocalDateTime.now());
sysUser.setUpdateBy(sysUser.getAccound());
sysUserService.update(sysUser);
//此处还可以进行一些处理,比如登录成功之后可能需要返回给前台当前用户有哪些菜单权限,
//进而前台动态的控制菜单的显示等,具体根据自己的业务需求进行扩展
// 根据用户的id和account生成token并返回
String jwtToken = jwtUtils.generateToken(sysUser.getAccound());
Map results = new HashMap<>();
results.put(jwtUtils.getHeader(),jwtToken);
//返回json数据
JsonResult result = ResultTool.success(results);
//处理编码方式,防止中文乱码的情况
httpServletResponse.setContentType("text/json;charset=utf-8");
//塞到HttpServletResponse中返回给前台
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
onAuthenticationFailure方法用于向前端返回错误信息,登录失败有可能是用户名密码错误,有可能是验证码错误,这里我们自定义了验证码错误的异常,它继承了Spring Security的AuthenticationException:
/**
* @Author: zm
* @Description:自定义验证码错误异常
* @Date: 2022/4/25 10:12
*/
public class CaptchaException extends AuthenticationException {
public CaptchaException(String msg) {
super(msg);
}
}
onAuthenticationFailure()方法里修改登录失败处理逻辑,添加验证码异常的失败处理
代码:
/**
* @Author: zm
* @Description:登录失败处理逻辑
* @Date: 2022/4/24 10:39
*/
@Component
public class CustomizeAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
//返回json数据
JsonResult result = null;
if (e instanceof CaptchaException) {
//验证码错误
result = ResultTool.fail(ResultCode.USER_CAPTCHA_ERROR);
} else if (e instanceof AccountExpiredException) {
//账号过期
result = ResultTool.fail(ResultCode.USER_ACCOUNT_EXPIRED);
} else if (e instanceof BadCredentialsException) {
//密码错误
result = ResultTool.fail(ResultCode.USER_CREDENTIALS_ERROR);
} else if (e instanceof CredentialsExpiredException) {
//密码过期
result = ResultTool.fail(ResultCode.USER_CREDENTIALS_EXPIRED);
} else if (e instanceof DisabledException) {
//账号不可用
result = ResultTool.fail(ResultCode.USER_ACCOUNT_DISABLE);
} else if (e instanceof LockedException) {
//账号锁定
result = ResultTool.fail(ResultCode.USER_ACCOUNT_LOCKED);
} else if (e instanceof InternalAuthenticationServiceException) {
//用户不存在
result = ResultTool.fail(ResultCode.USER_ACCOUNT_NOT_EXIST);
}else{
//其他错误
result = ResultTool.fail(ResultCode.COMMON_FAIL);
}
//处理编码方式,防止中文乱码的情况
httpServletResponse.setContentType("text/json;charset=utf-8");
//塞到HttpServletResponse中返回给前台
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
redis工具类网上有很多,我是用的是这个网址的RedisUtil: 最全的Java操作Redis的工具类,使用StringRedisTemplate实现,封装了对Redis五种基本类型的各种操作!
大家可以自行选择。如果自己项目有就用自己项目里的就可以。
验证码生成使用的是谷歌的验证码工具类,配置类如下:
/**
* @Author: zm
* @Description:验证码工具类
* @Date: 2022/4/25 10:17
*/
@Configuration
public class KaptchaConfig {
@Bean
DefaultKaptcha producer() {
Properties properties = new Properties();
properties.put("kaptcha.border", "no");
properties.put("kaptcha.textproducer.font.color", "black");
properties.put("kaptcha.textproducer.char.space", "4");
properties.put("kaptcha.image.height", "40");
properties.put("kaptcha.image.width", "120");
properties.put("kaptcha.textproducer.font.size", "30");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
@Autowired
private RedisUtil redisUtil;
@Autowired
Producer producer;
/**
* 获取验证码
* @return
* @throws IOException
*/
@GetMapping("/captcha")
public JsonResult Captcha() throws IOException {
String key = UUID.randomUUID().toString();
String code = producer.createText();
BufferedImage image = producer.createImage(code);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, "jpg", outputStream);
String str = "data:image/jpeg;base64,";
String base64Img = str + Base64.encodeBase64String(outputStream.toByteArray());
redisUtil.hPut(Constant.CAPTCHA, key, code);
return ResultTool.success(
MapUtil.builder()
.put("userKey", key)
.put("captcherImg", base64Img)
.build()
);
}
Constant是自定义常量池:
public class Constant { /** * 验证码常量 */ public final static String CAPTCHA="captcha"; }
在验证码过滤器中,需要先判断请求是否是登录请求,若是登录请求,则进行验证码校验,从redis中通过userKey查找对应的验证码,看是否与前端所传验证码参数一致,当校验成功时,因为验证码是一次性使用的,一个验证码对应一个用户的一次登录过程,所以需用hdel将存储的HASH删除。当校验失败时,则交给登录认证失败处理器LoginFailureHandler进行处理。
CaptchaFilter继承了OncePerRequestFilter抽象类,该抽象类在每次请求时只执行一次过滤,即它的作用就是保证一次请求只通过一次filter,而不需要重复执行。CaptchaFilter需要重写其doFilterInternal方法来自定义处理逻辑
/**
* @Author: zm
* @Description: 对请求进行过滤,判断JWT token是否有效
* 验证码过滤器
* @Date: 2022/4/24 17:08
*/
@Component
public class CaptchaFilter extends OncePerRequestFilter {
@Autowired
RedisUtil redisUtil;
@Autowired
CustomizeAuthenticationFailureHandler loginFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String url = request.getRequestURI();
if ("/login".equals(url) && request.getMethod().equals("POST")) {
// 校验验证码
try {
validate(request);
} catch (CaptchaException e) {
// 交给认证失败处理器
loginFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
filterChain.doFilter(request, response);
}
// 校验验证码逻辑
private void validate(HttpServletRequest httpServletRequest) {
String code = httpServletRequest.getParameter("code");
String key = httpServletRequest.getParameter("userKey");
if (StringUtils.isBlank(code) || StringUtils.isBlank(key)) {
throw new CaptchaException("验证码错误");
}
if (!code.equals(redisUtil.hGet(Constant.CAPTCHA, key))) {
throw new CaptchaException("验证码错误");
}
// 若验证码正确,执行以下语句
// 一次性使用
redisUtil.hDelete(Constant.CAPTCHA, key);
}
}
在首次登录成功后,LoginSuccessHandler将生成JWT,并返回给前端。在之后的所有请求中(包括再次登录请求),都会携带此JWT信息。我们需要写一个JWT过滤器JwtAuthenticationFilter,当前端发来的请求有JWT信息时,该过滤器将检验JWT是否正确以及是否过期,若检验成功,则获取JWT中的用户名信息,检索数据库获得用户实体类,并将用户信息告知Spring Security,后续我们就能调用security的接口获取到当前登录的用户信息。
若前端发的请求不含JWT,我们也不能拦截该请求,因为一般的项目都是允许匿名访问的,有的接口允许不登录就能访问,没有JWT也放行是安全的,因为我们可以通过Spring Security进行权限管理,设置一些接口需要权限才能访问,不允许匿名访问
/**
* @Author: zm
* @Description: JWT token过滤器
* @Date: 2022/4/25 11:47
*/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
JwtUtils jwtUtils;
@Autowired
SysUserService sysUserService;
/**
* 直接将我们前面写好的service注入进来,通过service获取到当前用户的权限
* */
@Autowired
private UserDetailsServiceImpl userDetailsService;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 获取当请求头中的token,其实这里多余,完全可以使用HttpServletRequest来获取
String authToken = request.getHeader(jwtUtils.getHeader());
// String jwt = request.getHeader(jwtUtils.getHeader());
// 获取到当前用户的account
String account = jwtUtils.getMemberAccountByJwtToken(request);
System.out.println("自定义JWT过滤器获得用户名为"+account);
Authentication a=SecurityContextHolder.getContext().getAuthentication();
// 当token中的username不为空时进行验证token是否是有效的token
if (!account.equals("") && SecurityContextHolder.getContext().getAuthentication() == null) {
// token中username不为空,并且Context中的认证为空,进行token验证
// 获取到用户的信息,也就是获取到用户的权限
UserDetails userDetails = this.userDetailsService.loadUserByUsername(account);
if (jwtUtils.checkToken(authToken)) { // 验证当前token是否有效
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//将authentication放入SecurityContextHolder中
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
// 放行给下个过滤器
chain.doFilter(request, response);
}
}
若JWT验证成功,我们构建了一个UsernamePasswordAuthenticationToken对象,用于保存用户信息,之后将该对象交给SecurityContextHolder,set进它的context中,这样后续我们就能通过调用SecurityContextHolder.getContext().getAuthentication().getPrincipal()等方法获取到当前登录的用户信息了。
/**
* @Author: Administrator
* @Description:jwt认证失败处理器
* @Date: 2022/4/25 12:00
*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
JsonResult result = ResultTool.fail(ResultCode.USER_NOT_LOGIN);
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(result));
}
}
/**
* @Author: zm
* @Description: 没有权限设置
* @Date: 2022/4/24 16:59
*/
@Component
public class CustomizeAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
JsonResult result = ResultTool.fail(ResultCode.NO_PERMISSION);
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(result));
}
}
因为是结合的前一篇文章,如果有没有的依赖,请参考前一篇文章:
SpringBoot整合Spring Security实现前后端分离登录权限处理_zmgst的博客-CSDN博客
WebSecurityConfig整体配置:
/**
* spring security 配置类
* @Author: zm
* @Description:
* @Date: 2022/4/22 13:48
*/
@Configuration
@EnableWebSecurity //开启Spring Security的功能
//prePostEnabled属性决定Spring Security在接口前注解是否可用@PreAuthorize,@PostAuthorize等注解,设置为true,会拦截加了这些注解的接口
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义用户登录操作
*/
@Autowired
private UserDetailsServiceImpl userDetailsService;
/**
* 匿名用户访问无权限资源时的异常
*/
@Autowired
private CustomizeAuthenticationEntryPoint authenticationEntryPoint;
/**
* 登录成功执行方法
*/
@Autowired
private CustomizeAuthenticationSuccessHandler authenticationSuccessHandler;
/**
* 登陆失败执行方法
*/
@Autowired
private CustomizeAuthenticationFailureHandler authenticationFailureHandler;
/**
* 没有权限设置
*/
@Autowired
private CustomizeAccessDeniedHandler customizeAccessDeniedHandler;
/**
* 登出成功执行方法
*/
@Autowired
private CustomizeLogoutSuccessHandler logoutSuccessHandler;
/**
* 会话过期策略处理
*/
@Autowired
private CustomizeSessionInformationExpiredStrategy sessionInformationExpiredStrategy;
//自定义权限访问设置 =======开始=========
/**
* 访问决策管理器
*/
@Autowired
private CustomizeAccessDecisionManager accessDecisionManager;
/**
* 安全元数据源
*/
@Autowired
private CustomizeFilterInvocationSecurityMetadataSource securityMetadataSource;
/**
* 权限拦截器
*/
@Autowired
private CustomizeAbstractSecurityInterceptor securityInterceptor;
//自定义权限访问设置 =======结束=========
/**
* 验证码过滤
*/
@Autowired
private CaptchaFilter captchaFilter;
/**
* JWT token过滤器
* @return
* @throws Exception
*/
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
return jwtAuthenticationFilter;
}
/**
* 指定加密方式
* @return
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable();
http
.authorizeRequests()
//自定义权限控制器
.withObjectPostProcessor(new ObjectPostProcessor() {
@Override
public O postProcess(O o) {
o.setAccessDecisionManager(accessDecisionManager);//访问决策管理器
o.setSecurityMetadataSource(securityMetadataSource);//安全元数据源
return o;
}
})
.antMatchers(HttpMethod.POST, "/sysUser/addUser").permitAll() // 允许post请求/add-user,而无需认证
.antMatchers("/sysUser/captcha").permitAll()//验证码放过
.anyRequest().authenticated() // 有请求都需要验证
//登入
.and().formLogin().
permitAll().//允许所有用户
successHandler(authenticationSuccessHandler).//登录成功处理逻辑
failureHandler(authenticationFailureHandler).//登录失败处理逻辑
//登出
and().logout().
permitAll().//允许所有用户
logoutSuccessHandler(logoutSuccessHandler)//登出成功处理逻辑
// deleteCookies("JSESSIONID")//登出之后删除cookie
//异常处理(权限拒绝、登录失效等)
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)//匿名用户访问无权限资源时的异常处理
.accessDeniedHandler(customizeAccessDeniedHandler)
// 无状态session,不进行存储 禁用session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//设置一个账号只能一个用户使用
// .maximumSessions(1)
// //会话信息过期策略会话信息过期策略(账号被挤下线)
// .expiredSessionStrategy(sessionInformationExpiredStrategy)
// 配置自定义的过滤器
.and()
.addFilter(jwtAuthenticationFilter())
// 验证码过滤器放在UsernamePassword过滤器之前
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class)
;
http.addFilterBefore(securityInterceptor,FilterSecurityInterceptor.class);
// http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
验证码接口:
访问后会返回唯一标识和base64编码的图片,这里我直接从redis里获取了
redis里的验证码:
访问登录接口:
提示请求成功,返回了一个token
将刚才返回的token添加到请求头上,进行请求,返回成功
此文章只是简单操作了Spring Security整合jwt的流程,具体的原理并没有讲解,望各位大佬多多指教!