SpringBoot SpringSecurity JWT实现系统安全策略详解
security进行用户验证和授权;jwt负责颁发令牌与校验,判断用户登录状态
一、原理
1. SpringSecurity过滤器链
SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。
- SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中。
- LogoutFilter:用于处理退出登录。
- UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。
- BasicAuthenticationFilter:检测和处理 http basic 认证。
- ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。
- FilterSecurityInterceptor:可以看做过滤器链的出口。
- …
流程说明:客户端发起一个请求,进入 Security 过滤器链。
当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。
当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层否则到 AccessDeniedHandler 鉴权失败处理器处理。
2. JWT校验
首先前端一样是把登录信息发送给后端,后端查询数据库校验用户的账号和密码是否正确,正确的话则使用jwt生成token,并且返回给前端。以后前端每次请求时,都需要携带token,后端获取token后,使用jwt进行验证用户的token是否无效或过期,验证成功后才去做相应的逻辑。
二、Security+JWT配置说明
1. 添加maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2. securityConfig配置
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
LoginFailureHandler loginFailureHandler;
@Autowired
LoginSuccessHandler loginSuccessHandler;
@Autowired
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
JwtAccessDeniedHandler jwtAccessDeniedHandler;
@Autowired
UserDetailServiceImpl userDetailService;
@Autowired
JWTLogoutSuccessHandler jwtLogoutSuccessHandler;
@Autowired
CaptchaFilter captchaFilter;
@Value("${security.enable}")
private Boolean securityIs = Boolean.TRUE;
@Value("${security.permit}")
private String permit;
@Bean
public HttpFirewall allowUrlEncodedSlashHttpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
//此处可添加别的规则,目前只设置 允许双 //
firewall.setAllowUrlEncodedDoubleSlash(true);
return firewall;
}
@Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager(), jwtAuthenticationEntryPoint);
return jwtAuthenticationFilter;
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.cors().and().csrf().disable()
// 登录配置
.formLogin()
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
.and()
.logout()
.logoutSuccessHandler(jwtLogoutSuccessHandler)
// 禁用session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 配置拦截规则
.and()
.authorizeRequests()
.antMatchers(permit.split(",")).permitAll();
if (!securityIs) {
http.authorizeRequests().antMatchers("/**").permitAll();
}
registry.anyRequest().authenticated()
// 异常处理器
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
// 配置自定义的过滤器
.and()
.addFilter(jwtAuthenticationFilter())
// 验证码过滤器放在UsernamePassword过滤器之前
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailService);
}
}
3. JwtAuthenticationFilter校验token
package cn.piesat.gf.filter;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.piesat.gf.dao.user.SysUserDao;
import cn.piesat.gf.model.entity.user.SysUser;
import cn.piesat.gf.exception.ExpiredAuthenticationException;
import cn.piesat.gf.exception.MyAuthenticationException;
import cn.piesat.gf.service.user.impl.UserDetailServiceImpl;
import cn.piesat.gf.utils.Constants;
import cn.piesat.gf.utils.JwtUtils;
import cn.piesat.gf.utils.Result;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Slf4j
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
private AuthenticationEntryPoint authenticationEntryPoint;
private AuthenticationManager authenticationManager;
@Autowired
JwtUtils jwtUtils;
@Autowired
UserDetailServiceImpl userDetailService;
@Autowired
SysUserDao sysUserRepository;
@Autowired
RedisTemplate redisTemplate;
@Value("${security.single}")
private Boolean singleLogin = false;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) {
super(authenticationManager, authenticationEntryPoint);
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null");
this.authenticationManager = authenticationManager;
this.authenticationEntryPoint = authenticationEntryPoint;
}
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String jwt = request.getHeader(jwtUtils.getHeader());
// 这里如果没有jwt,继续往后走,因为后面还有鉴权管理器等去判断是否拥有身份凭证,所以是可以放行的
// 没有jwt相当于匿名访问,若有一些接口是需要权限的,则不能访问这些接口
if (StrUtil.isBlankOrUndefined(jwt)) {
chain.doFilter(request, response);
return;
}
try {
Claims claim = jwtUtils.getClaimsByToken(jwt);
if (claim == null) {
throw new MyAuthenticationException("token 异常");
}
if (jwtUtils.isTokenExpired(claim)) {
throw new MyAuthenticationException("token 已过期");
}
String username = claim.getSubject();
Object o1 = redisTemplate.opsForValue().get(Constants.TOKEN_KEY + username);
String o = null;
if(!ObjectUtils.isEmpty(o1)){
o = o1.toString();
}
if (!StringUtils.hasText(o)) {
throw new ExpiredAuthenticationException("您的登录信息已过期,请重新登录!");
}
if (singleLogin && StringUtils.hasText(o) && !jwt.equals(o)) {
throw new MyAuthenticationException("您的账号已别处登录,您已下线,如有异常请及时修改密码!");
}
// 获取用户的权限等信息
SysUser sysUser = sysUserRepository.findByUserName(username);
// 构建UsernamePasswordAuthenticationToken,这里密码为null,是因为提供了正确的JWT,实现自动登录
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, userDetailService.getUserAuthority(sysUser.getUserId()));
SecurityContextHolder.getContext().setAuthentication(token);
chain.doFilter(request, response);
} catch (AuthenticationException e) {
log.error(ExceptionUtil.stacktraceToString(e));
authenticationEntryPoint.commence(request, response, e);
return;
} catch (Exception e){
log.error(ExceptionUtil.stacktraceToString(e));
response.getOutputStream().write(JSONUtil.toJsonStr(Result.fail(e.getMessage())).getBytes(StandardCharsets.UTF_8));
response.getOutputStream().flush();
response.getOutputStream().close();
return;
}
}
}
4. JWT生成与解析工具类
package cn.piesat.gf.utils;
import cn.hutool.core.exceptions.ExceptionUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
@Data
@Component
@ConfigurationProperties(prefix = "jwt.config")
@Slf4j
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)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
// 解析JWT
public Claims getClaimsByToken(String jwt) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(jwt)
.getBody();
} catch (Exception e) {
log.error(ExceptionUtil.stacktraceToString(e));
return null;
}
}
// 判断JWT是否过期
public boolean isTokenExpired(Claims claims) {
return claims.getExpiration().before(new Date());
}
}
到此这篇关于SpringBoot SpringSecurity JWT实现系统安全策略详解的文章就介绍到这了,更多相关SpringBoot SpringSecurity JWT内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341