JWT
1.什么是JWT
JWT是一种JSON类型的token令牌,用于各方将信息作为JSON对象进行安全传输,传输过程中还可进行数据加密、签名等处理
2.JWT的结构
JWT由三部分组成
- Header 标头
- 通常由令牌类型(如JWT)和所使用的签名算法组成
- Payload 负载
- 通常搭载非敏感信息,如用户id、名称等常用非敏感信息,将其保存在客户端,以减少向服务端请求
- Signature 签名
- 该部分会使用base64编码后的Header、Payload以及服务端提供的密钥使用Header中指定的签名算法进行加密,判断信息是否被篡改主要就是通过该部分
前两部分会通过Base64编码加密,但这是可逆的,故不会存放敏感信息
3.基于token的鉴权机制
用户登录请求的流程通常为:
1.用户输入账号密码传给服务器
2.服务器对账号密码进行匹配验证
3.验证成功后将用户部分非敏感信息插入Payload生成token返回给客户端
4.客户端将接收到的token保存在本地,每次请求数据时在请求头附上token
5.服务端对请求头中的token进行签名验证,验证通过则放行
鉴权系统-1
根据上文的鉴权流程,结合我的网站需求我们可以得到如下设计
- 前端:
- 增添路由守卫,在路由跳转前检测是否已获取token
需注意该检测并不能阻止用户继续访问页面,因为本地存储用户是可修改的,只能配合后端拦截做到不返回数据
- 需要将登录请求返回的token保存,请求时将token加入请求头
- 增加角色检测,在组件渲染前先判断角色是否有权限
- 后端:
- 编写JWT工具类,简化token生成以及验证调用
- 对于非公开数据请求都进行签名验证
- 对用户的请求还需要鉴权
Spring Security
仅仅通过JWT,我们只能实现用户认证,即用户是否能访问该系统,简单说就是是否登录了, 我们还需要对用户进行权限控制,即用户是否有权限访问某个接口。为了解决这个问题,我引入了Spring Security,基于Spring的安全实现
认证流程
- AuthencationoManager:定义了认证authentication的方法
- Authentication:封装用户相关信息的对象
- UserDetailsService:加载用户特定数据的接口
- UserDetails:Service将查询到的数据封装为UserDetails对象返回,然后再将这些信息封装到Authentication中
三个核心组件
- Authentication:存储认证信息
- SecurityContext:上下文对象,用来获取Authentication
- SecurityContextHolder:上下文管理对象,用于获取SecurityContext
由此可见,要实现自己的认证业务,主要关注DaoAuthentication以及UserDetailsService的实现,后者用于查询用户信息,前者用于对查询到的用户信息进行验证封装,当AuthencationoManager调用authenticate方法时,该方法会调用UserDetailsService的loadUserByUsername,当我们需要鉴权时,如检测用户是否有某角色,我们便可以重写loadUserByUsername方法,将用户用有角色一同封装返回
但在实际情况中,我们并不希望每写一个接口就调用一次authenticate验证,所以我们需要编写一个过滤器,过滤器中我们可以直接解析token,从中获取userId,然后获取用户信息以及权限列表(角色列表),直接将其封装到Authentication的实现类UsernamePasswordAuthenticationToken中,存入上下文,这样做可以直接使用Spring Security原生的@PreAuthorize等注解,依赖于UsernamePasswordAuthenticationToken中的authorities(我们权限列表就是存入这里)进行用户权限判断
鉴权系统-2
根据对Spring Security的分析,我们可以得到后端的进一步设计
- 装载Spring Security
- 配置哪些接口需要权限控制
- 编写过滤器
- 解析token是否合法
- 获取用户权限封装至UsernamePasswordAuthenticationToken并存入上下文
- 异常处理,捕捉权限不足抛出的错误
AccessDeniedException
装载了Spring Security后,我们也可以减少前端关于当前用户部分信息的传输,如用户id,我们可以直接从ss线程中的上下文获取
具体实现
SecurityConfig
- 配置跨域源
- 权限控制接口
- 配置Filter
@Configuration
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
public class SecurityConfig {
@Autowired
TokenFilter tokenFilter;
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(auth ->
auth
.requestMatchers("/public").permitAll()//公共接口都放行
.anyRequest().permitAll())//其他接口通过注解@@PreAuthorize进行权限控制
.csrf(AbstractHttpConfigurer::disable);
http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);//配置Filter
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("http://localhost:8081","https://momoyouta.github.io","https://www.momoyouta.fun")); // 允许源
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true); // 允许携带 Cookie
config.setMaxAge(3600L); // 预检请求的缓存时间
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config); // 对所有路径生效
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}
TokenFilter
- 解析JWT,验证合法性
- 获取用户缓存用信息,存入上下文
@Component
@Slf4j
public class TokenFilter extends OncePerRequestFilter {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = request.getHeader(KeyProperties.TOKEN_HEADER);
if(token == null||token.isEmpty()) { //无token
filterChain.doFilter(request, response);
return;
}
Claims claims = null;
try {
claims = JwtUtil.parseJWT(token);
} catch (ExpiredJwtException e) { //异常处理
String errorInfo = "accessToken已经过期";
WebUtil.renderString(response, errorInfo); //立即返回请求工具类,直接返回错误信息
return;
} catch (Exception e) { //异常处理
String errorInfo = "accessToken解析失败";
WebUtil.renderString(response, errorInfo);
return;
}
//需注意JSON解析,存入JWT时存入JSON,方便解析
UserCacheVO userCacheVO = objectMapper.readValue(claims.getSubject(), UserCacheVO.class);
String userId=userCacheVO.getId();
List<GrantedAuthority> authorities = new ArrayList<>();
for(String role: userCacheVO.getIdentities()){
authorities.add(new SimpleGrantedAuthority(role));
}//获取权限列表如['user','admin',...]
UsernamePasswordAuthenticationToken authenticationToken = //封装Authentication实现类并存入上下文
new UsernamePasswordAuthenticationToken(userId, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
异常处理
- 权限不足异常
AccessDeniedException
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
public Result<String> handleAccessDeniedException(AccessDeniedException e) {
return Result.error(2,"无权限访问");
}//Result为自定义响应体
}