Spring Security + JWT 认证学习
之前学习认证授权相关知识,先了解学习了shiro,最近在学习Spring Security,同时了解到分布式认证方式JWT,所以尝试结合两者写一段demo
,算是同时加深对两者的理解和认识。
JWT工具类
封装JwtUtils
.
public final class JwtUtils {
private final static String SECRET_KEY = "flyzzfighting";
//生成jwt token
public static String generate(Map<String,Object> map,Duration duration) {
Date expiryDate = new Date(System.currentTimeMillis()+duration.toMillis());
return Jwts.builder().setSubject("token").setClaims(map).setIssuedAt(new Date()).setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512,SECRET_KEY).compact();
}
//解析jwt token
public static Claims parse(String token) {
if(!StringUtils.hasLength(token)) {
return null;
}
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
} catch (JwtException e) {
log.info(e.toString());
}
return claims;
}
}
这样封装只是为了获取token,对于JWT解析可能产生各种异常,如超时,签名错误等没有分开处理。
Spring Security配置
自定义配置类
一般定义如下类,可以对Spring Security进行配置。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//禁用Session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
Authenciation 认证
自定义认证类,可以参考UsernamePasswordAuthenticationFilter
进行编写,这里继承UsernamePassword
的父类重写,在attemptAuthentication
方法中重写认证逻辑,之后在successfulAuthentication
方法中编写认证成功后的逻辑,这里是在响应头中添加token
,不过写法有点烂,主要是对java 相关更优API不太熟悉,以及对jackson对于List容器的序列化不太熟悉,之后有待学习,这里在access_token
中存入了userName
和该用户的权限authority
,refresh_token
中存入了userName
。另外,在request域中放入userName
用于告诉Controller
已认证,并可以通过userName
查询数据库,处理后返回相应DTO
,最后配好SecurityContext
中的Authentication。
public class CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/api/login", "POST");
private AuthenticationManager authenticationManager;
protected CustomAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
super.setAuthenticationManager(authenticationManager );
}
static class UserInfo{
public String userName;
public String password;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if(!"POST".equals(request.getMethod())) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
ObjectMapper objectMapper = new ObjectMapper();
UserInfo userInfo = null;
try {
userInfo = objectMapper.readValue(request.getInputStream(),UserInfo.class);
} catch (IOException e) {
e.printStackTrace();
}
assert userInfo != null;
if(userInfo.userName==null) {
throw new AuthenticationServiceException("用户名不能为空");
} else {
userInfo.userName = userInfo.userName.trim();
userInfo.password = userInfo.password.trim();
return super.getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(userInfo.userName,userInfo.password));
}
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
Map<String,Object> accessMap = new HashMap<>();
Map<String,Object> refreshMap = new HashMap<>();
User user = (User) authResult.getPrincipal();
accessMap.put("userName",user.getUsername());
refreshMap.put("userName",user.getUsername());
accessMap.put("authority",authResult.getAuthorities());
request.setAttribute("userName", user.getUsername());
response.setHeader("accessToken",JwtUtils.generate(accessMap,Duration.ofMinutes(3)));
response.setHeader("refreshToken",JwtUtils.generate(refreshMap,Duration.ofMinutes(9)));
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request,response);
}
}
重写后,需要用它替代默认的UsernamePasswordAuthentication
,在配置类中配置:
http.addFilterAt(new CustomAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
这样装配的话,其中的AuthenticationManager
是需要自己注入的,于是写了一个带参构造方法传入,同时还要为AuthenticationManager
配置UserDetailsService
和PasswordEncoder
。
@Component
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
UserService userService;
@Autowired
AuthorityService authorityService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
UserDO userDO = userService.loadUserByName(s);
if(userDO!=null) {
List<AuthorityDO> list = authorityService.getAuthorityListById(userDO.getId());
List<GrantedAuthority> list1 = new ArrayList<>();
for (AuthorityDO authorityDO:list) {
list1.add(new SimpleGrantedAuthority(authorityDO.getName()));
}
return new User(userDO.getUserName(),userDO.getPassword(),list1);
} else {
throw new UsernameNotFoundException("用户名不存在");
}
}
}
//配置类中
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
在配置类中为AuthenticationManager
配置:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
//在构造方法中传参
@Override
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
//加入到对应configure中
http.addFilterAt(new CustomAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
认证完成
Authorization 授权
编写相关Filter
,777
的前缀是为了不让它拦截到refresh_token
,所以发请求时如果是access_token
要带上777前缀。大概就是解析token
,得到用户名和相关权限,装到SecurityContext
中。
public class JwtTokenFilter extends OncePerRequestFilter {
private final String MY_SIGN = "777 ";
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
Claims claims = null;
if(httpServletRequest.getHeader("Authorization")!=null&&httpServletRequest.getHeader("Authorization").startsWith(MY_SIGN)) {
claims = JwtUtils.parse(httpServletRequest.getHeader("Authorization").substring(MY_SIGN.length()));
}
if(!ObjectUtils.isEmpty(claims)) {
String userName = claims.get("userName",String.class);
List list = claims.get("authority", List.class);
List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
for (Object o : list) {
authorityList.add(new SimpleGrantedAuthority((String) ((LinkedHashMap) o).get("authority")));
}
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userName,"",authorityList);
SecurityContextHolder.getContext().setAuthentication(token);
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
之后把过滤器加到AnonymousAuthenticationFilter
前:
http.addFilterBefore(new JwtTokenFilter(), AnonymousAuthenticationFilter.class);
Refresh Token
大概就是检查token,合法就重新发放token. 通过refresh_token
中的userName查询数据库,重新签名,发放JWT token。
@GetMapping("/refreshtoken")
public String refresh(@RequestHeader(value = "Authorization",required = false) String token, HttpServletResponse response) {
if(token==null) {
return "请重新登录";
} else {
Claims claims = JwtUtils.parse(token);
if(claims==null) {
return "请重新登录";
}
String userName = claims.get("userName",String.class);
UserDO userDO = userService.loadUserByName(userName);
List<AuthorityDO> list = authorityService.getAuthorityListById(userDO.getId());
List<GrantedAuthority> list1 = new ArrayList<>();
for (AuthorityDO authorityDO:list) {
list1.add(new SimpleGrantedAuthority(authorityDO.getName()));
}
Map<String,Object> accessMap = new HashMap<>();
Map<String,Object> refreshMap = new HashMap<>();
accessMap.put("userName",userName);
refreshMap.put("userName",userName);
accessMap.put("authority",list1);
response.setHeader("accessToken",JwtUtils.generate(accessMap, Duration.ofMinutes(30)));
response.setHeader("refreshToken",JwtUtils.generate(refreshMap,Duration.ofMinutes(90)));
return "刷新成功";
}
}
简单异常处理
对于认证或权限失败,可以编写处理类实现AuthenticationEntryPoint
或AccessDeniedHandler
接口,并配置。
http.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint());
http.exceptionHandling().accessDeniedHandler(new CustomAccessDenyEntryPoint());
Configure完整
@Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterAt(new CustomAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(new JwtTokenFilter(), AnonymousAuthenticationFilter.class);
http.authorizeRequests().antMatchers("/api/login","/api/refreshtoken").permitAll();
http.authorizeRequests().anyRequest().authenticated();
http.csrf().disable();
http.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint());
http.exceptionHandling().accessDeniedHandler(new CustomAccessDenyEntryPoint());
}
安全注解的使用
在配置类上使用@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
注解。
@PreAuthorize("hasAuthority(\"HELLO\")")
@Secured({"ROLE_ADMIN"})
@Secured
:只能检查role,即要有前缀ROLE_
。
杂记
-
连接数据库时出现异常,提示Public Key Retrival错误。
-
使用java11,mybatis,由于java11移除了了javaEE 中xml支持,会报错。
添加依赖:
//我只添加了第一个依赖就行了,stackoverflow上面给出的是这三个依赖。 <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-core</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-impl</artifactId> <version>2.3.0</version> </dependency>