springboot-安全认证security+jwt总结
2023-09-11 14:19:58 时间
目录
一、背景
要做一个后台管理系统,会引入多个系统,这就需要做用户认证和权限管理。用户认证通过token来实现,市面上的技术有很多,我这里仅仅来说明一下security+jwt的一种实现过程,没有做页面,需要做页面的同学自行实现。
有些容易入坑的点,我看别的资料没有说太清楚,这里记录下,希望能帮助到跳坑的同学。
二、基本jar依赖引入
<!-- security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- jwt依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
总结:
我看有的帖子也引入了jjwt的API、impl包,我这里没有用到,实现权限控制和token校验两个完全够用
三、security模块
1、编写配置类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//操作用户
@Autowired
FfUserService ffUserService;
//token校验
@Autowired
JwtAuthenPreFilter jwtAuthenPreFilter;
/**
*token异常
*/
@Autowired
UnauthorizedHandler unauthorizedHandler;
//配置放行策略
@Value("${jwt.security.antMatchers}")
private String antMatchers;
/**
* 密码加密算法
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
/**
* 验证用户来源,主要是验证账号和密码
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(ffUserService).passwordEncoder(passwordEncoder());
}
/**
* 忽略策略
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(antMatchers.split(","));
}
/**
* 用户授权
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated() // 所有的验证都需要验证
.and()
.csrf().disable() // 禁用 Spring Security 自带的跨域处理
// 定制我们自己的 session 策略:调整为让 Spring Security 不创建和使用 session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler);
// 将自定义的过滤器添加在指定过滤器之前
http.addFilterBefore(jwtAuthenPreFilter, FilterSecurityInterceptor.class);
// 禁用缓存
http.headers().cacheControl();
}
}
总结:
- 需要放行的可以在两个地方配置,第一种如上图;第二种可以在第二个configure中配置。比如:.antMatchers(antMatchers.split(",")).permitAll()
- 在第二个configure中这里特别注意一下配置的顺序,exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint())如果放在前面会导致放行策略不生效。
- security是通过用户名和密码来实现认证的,不一定能满足实际业务需要,所以要扩展,前后端分离目前常用的做法就是基于usertoken的,即上面的自定义的jwtAuthenPreFilter过滤器
- addFilterAfter: 将自定义的过滤器添加在指定过滤器之后
- addFilterBefore:将自定义的过滤器添加在指定过滤器之前
- addFilter:添加一个过滤器,但必须是Spring Security自身提供的过滤器实例或其子过滤器
- addFilterAt: 添加一个过滤器在指定过滤器位置
2、UnauthorizedHandler代码
@Component
@Slf4j
public class UnauthorizedHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
//用户登录时身份认证未通过
if(e instanceof BadCredentialsException){
//用户登录时身份认证失败
ResultUtil.writeJavaScript(httpServletResponse, ErrorCodeEnum.TOKEN_INVALID.getCode(), e.getMessage());
}else if (e instanceof InsufficientAuthenticationException){
//缺少请求头参数,Authorization传递是token值,所以是参数是必须的
ResultUtil.writeJavaScript(httpServletResponse, ErrorCodeEnum.NO_TOKEN.getCode(), ErrorCodeEnum.NO_TOKEN.getMessage());
}else{
//用户token无效
ResultUtil.writeJavaScript(httpServletResponse, ErrorCodeEnum.TOKEN_INVALID.getCode(), ErrorCodeEnum.TOKEN_INVALID.getMessage());
}
}
}
注意:
- 接口中的逻辑异常要捕获,不然会被拦截报token异常就不美观了,也可以完善这个类。
- 也可以扩展单独的异常处理模块做统一处理,但是业务异常我还是推荐根据业务场景来单独处理,一味的追求统一处理不见得都是好事。
3、security验证用户名和密码的部分
- 网上资料很多,大家自己补充
四、jwt模块
1、jwt原理部分
- 网上资料很多,大家自己补充
2、jwt一共需要四个类
- JwtAuthenPreFilter:token校验和有关业务,这部分可以根据自己项目来实现
@Component
@Slf4j
public class JwtAuthenPreFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
//@Autowired
//private RedisUtil redisUtil;
/**
* 防止filter被执行两次
*/
private static final String FILTER_APPLIED = "__spring_security_JwtAuthenPreFilter_filterApplied";
@Value("${jwt.header:Authorization}")
private String tokenHeader;
@Value("${jwt.tokenHead:Bearer}")
private String tokenHead;
/**
* 距离快过期多久刷新令牌
*/
@Value("${jwt.token.subRefresh:#{10*60}}")
private Long subRefresh;
// 不需要认证的接口
@Value("${jwt.security.antMatchers}")
private String antMatchers;
@Autowired
private FfUserService ffUserService ;
public JwtAuthenPreFilter() {
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
if (httpServletRequest.getAttribute(FILTER_APPLIED) != null) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
httpServletRequest.setAttribute(FILTER_APPLIED, true);
//过滤掉不需要token验证的url
SkipPathAntMatcher skipPathRequestMatcher = new SkipPathAntMatcher(Arrays.asList(antMatchers.split(",")));
if (skipPathRequestMatcher.matches(httpServletRequest)) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
} else {
try {
//1.判断是否有效 2.判断是否过期 3.如果未过期的,且过期时间小于10分钟的延长过期时间,并在当前response返回新的header,客户端需替换此令牌
String authHeader = httpServletRequest.getHeader(this.tokenHeader);
if (authHeader != null && authHeader.startsWith(tokenHead)) {
final String authToken = authHeader.substring(tokenHead.length());
JWTUserDetail userDetail = jwtTokenUtil.getUserFromToken(authToken);
if (ObjectUtils.isEmpty(userDetail)) {
log.info("令牌非法,解析失败{}!", authToken);
throw new BadCredentialsException(ErrorCodeEnum.TOKEN_INVALID.getMessage());
}
if (jwtTokenUtil.isTokenExpired(authToken)) {
log.info("令牌已失效!{}", authToken);
throw new BadCredentialsException(ErrorCodeEnum.TOKEN_INVALID.getMessage());
}
//令牌快过期生成新的令牌并设置到返回头中,客户端在每次的restful请求如果发现有就替换原值
if (new Date(System.currentTimeMillis() - subRefresh).after(jwtTokenUtil.getExpirationDateFromToken(authToken))) {
String resAuthToken = jwtTokenUtil.generateToken(userDetail);
httpServletResponse.setHeader(tokenHeader, tokenHead + resAuthToken);
}
JwtTokenUtil.LOCAL_USER.set(userDetail);
UserDetails userDetails = ffUserService.loadUserByUsername(userDetail.getLoginName());
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
//需要校验却无用户token
log.info("无header请求-->" + httpServletRequest.getRequestURI());
throw new InsufficientAuthenticationException(ErrorCodeEnum.NO_TOKEN.getMessage());
}
} catch (Exception e) {
//log.info("令牌解析失败!", e);
throw new BadCredentialsException(ErrorCodeEnum.TOKEN_INVALID.getMessage());
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
//调用完成后清除
JwtTokenUtil.LOCAL_USER.remove();
}
}
}
- JWTUserDetail:token转换成user类
@Data
public class JWTUserDetail implements Serializable {
/**
* 登陆用户编号
*/
private long userId;
/**
* 登陆用户账户名称(可能为手机号邮箱或者名称用户维度唯一)
*/
private String loginName;
/**
* 登陆用户类型
*/
private UserType userType;
/**
* 登陆用户凭证
*/
private String jwtToken;
/**
* 登陆时间
*/
private Date loginTime;
private static ObjectMapper mapper = new ObjectMapper();
public enum UserType {
User("USER", 1),
Operator("OPT", 2),
Erp("ERP", 3);
private String name;
private int index;
private UserType(String name, int index) {
this.name = name;
this.index = index;
}
@Override
public String toString() {
return this.name;
}
public static String getName(int index) {
for (UserType c : UserType.values()) {
if (c.getIndex() == index) {
return c.getName();
}
}
return null;
}
public String getName() {
return name;
}
public int getIndex() {
return index;
}
}
public static JWTUserDetail fromJson(String json) throws JsonProcessingException {
return mapper.readValue(json,JWTUserDetail.class);//JSONObject.parseObject(json, JWTUserDetail.class);
}
public String toJson() throws JsonProcessingException {
return mapper.writeValueAsString(this);//.toJSONString(this);
}
}
- JwtTokenUtil:token工具
@Component
public class JwtTokenUtil implements Serializable {
private static final long serialVersionUID = -5883980282405596071L;
public static final ThreadLocal<JWTUserDetail> LOCAL_USER = new ThreadLocal<>();
public final static String JWT_TOKEN_PREFIX = "jwt:%s:%d";
private final String JWT_LOGIN_NAME = "JWT_LOGIN_NAME";
private final String JWT_LOGIN_TIME = "JWT_LOGIN_TIME";
private final String JWT_LOGIN_USERID = "JWT_LOGIN_USERID";
private final String JWT_LOGIN_USERTYPE = "JWT_LOGIN_USERTYPE";
//签名方式
private final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
//密匙
@Value("${jwt.security.secret}")
private String secret;
@Value("${jwt.access_token:#{30*24*60*60}}")
private Long access_token_expiration;
public String getLoginNameFromToken(String token) {
return getClaimsFromToken(token).getSubject();
}
/**
* 根据token 获取用户信息
*/
public JWTUserDetail getUserFromToken(String token) {
JWTUserDetail jwtUserDetails = new JWTUserDetail();
Claims claims = getClaimsFromToken(token);
jwtUserDetails.setUserId(claims.get(JWT_LOGIN_USERID, Long.class));
jwtUserDetails.setLoginName(claims.get(JWT_LOGIN_NAME, String.class));
jwtUserDetails.setUserType(Enum.valueOf(JWTUserDetail.UserType.class, (String) claims.get(JWT_LOGIN_USERTYPE)));
jwtUserDetails.setLoginTime(new Date(claims.get(JWT_LOGIN_TIME, Long.class)));
jwtUserDetails.setJwtToken(token);
return jwtUserDetails;
}
/**
* 根据用户信息生成token
*
* @param user
* @return
*/
public String generateToken(JWTUserDetail user) {
Map<String, Object> claims = new HashMap<>();
claims.put(JWT_LOGIN_NAME, user.getLoginName());
claims.put(JWT_LOGIN_TIME, user.getLoginTime());
claims.put(JWT_LOGIN_USERID, user.getUserId());
claims.put(JWT_LOGIN_USERTYPE, user.getUserType());
return Jwts.builder()
//一个map 可以资源存放东西进去
.setClaims(claims)
// 用户名写入标题
.setSubject(user.getLoginName())
.setId(UUID.randomUUID().toString())
.setIssuedAt(new Date())
//过期时间
.setExpiration(new Date(System.currentTimeMillis() + access_token_expiration * 1000))
//数字签名
.signWith(SIGNATURE_ALGORITHM, secret)
.compact();
}
/**
* 根据token 获取生成时间
*/
public Date getCreatedDateFromToken(String token) {
return getClaimsFromToken(token).getIssuedAt();
}
/**
* 根据token 获取过期时间
*/
public Date getExpirationDateFromToken(String token) {
return getClaimsFromToken(token).getExpiration();
}
/**
* token 是否过期
*/
public Boolean isTokenExpired(String token) {
return getExpirationDateFromToken(token).before(new Date());
}
/***
* 解析token 信息
* @param token
* @return
*/
private Claims getClaimsFromToken(String token) {
return Jwts.parser()
//签名的key
.setSigningKey(secret)
// 签名token
.parseClaimsJws(token)
.getBody();
}
}
- SkipPathAntMatcher:token校验的放行策略
@Slf4j
public class SkipPathAntMatcher implements RequestMatcher {
private List<String> pathsToSkip;
public SkipPathAntMatcher(List<String> pathsToSkip) {
this.pathsToSkip = pathsToSkip;
}
@Override
public boolean matches(HttpServletRequest request) {
if (!ObjectUtils.isEmpty(pathsToSkip)) {
for (String s : pathsToSkip) {
AntPathRequestMatcher antPathRequestMatcher = new AntPathRequestMatcher(s);
if (antPathRequestMatcher.matches(request)) {
return true;
}
}
}
return false;
}
}
五、总结
- 放行策略有两个地方一定要都配置,1.security要配置,2.token的过滤器也要配置。
- security是校验URL是否有权限访问或者直接放行,我这里没有写用户角色和权限,是因为后面我计划给不同的角色返回不同的菜单,通过菜单来区分。如果你的业务需要可以单独设置
- token过滤器是配置这次请求是否token有效。security放行的,token也要放行。比如登录和注册页面,这时用户没有任何权限的,所以都放行。
最后,欢迎大家关注我的个人公众号,我会把经历分享出来,助你了解圈内圈外事。
同时也欢迎大家添加个人微信【shishuai860505】,我拉大家进我的读者交流群。
相关文章
- springboot引入第三方jar方式,使用scope:system配置systemPath编译,不用添加到本地仓库!
- JavaWeb-SpringBoot_使用H2数据库实现用户注册登录
- 补习系列(15)-springboot 分布式会话原理
- SpringBoot使用Mybatis注解进行一对多和多对多查询(2)
- SpringBoot ( 七 ) :springboot + mybatis 多数据源最简解决方案
- SpringBoot面试题及答案整理
- Springboot+Nginx前后端分离Https部署
- mybatis的mapper返回map结果集(springboot)
- Springboot中日期类型参数:转换处理
- SpringBoot-MongoDB 索引冲突分析及解决
- 基于注解SpringAOP,AfterReturning,Before,Around__springboot工程 @Around 简单的使用__SpringBoot:AOP 自定义注解实现日志管理
- SpringBoot 3.0最低版本要求的JDK 17,这几个新特性不能不知道
- SpringBoot中使用Shiro和JWT做认证和鉴权
- 编程实践精华总结集锦系列2: SpringBoot/Maven/IDEA/Java/Kotlin/Redis等等
- 好玩的ES--第三篇之过滤查询,整合SpringBoot
- SpringBoot入门:SpringBoot项目属性配置:2种配置风格(.properties风格和yml风格)+ 2种开发环境(dev开发环境和prod生产环境)
- 【springboot】5、lombok