需求
1、使用用户名和密码进行用户认证,成功返回token
2、拦截请求,验证token,更新token,返回新token
实现思路
生成UsernamePasswordAuthenticationToken,并调用AuthenticationManager的authenticate方法验证用户身份
方案一:
编写一个继承AbstractAuthenticationProcessingFilter的filter。首先在ter里实现一个RequestMatcher,指定拦截的url和http方法。然后重写attemptAuthentication方法,提取用户名和密码,实例化一个UsernamePasswordAuthenticationToken实例,以实例作为参数调用AuthenticationManager的authenticate方法,并返回方法调用的结果。
public class MyUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public MyUsernamePasswordAuthenticationFilter() {
//拦截url为 "/login" 的POST请求
super(new AntPathRequestMatcher("/login", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
//从json中获取username和password
String body = StreamUtils.copyToString(request.getInputStream(), Charset.forName("UTF-8"));
String username = null, password = null;
if(StringUtils.hasText(body)) {
JSONObject jsonObj = JSON.parseObject(body);
username = jsonObj.getString("username");
password = jsonObj.getString("password");
}
if (username == null)
username = "";
if (password == null)
password = "";
username = username.trim();
//封装到token中提交
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
方案二:
编写一个验证方法,接受参数grant_type和JSONObject。grant_type传入Oauth支持的5类模式名之一,JSONObject传入验证信息。根据grant_type的类型,使用JSONObject生成一个Authentication实现类实例,这里是UsernamePasswordAuthenticationToken。以实例作为参数传入AuthenticationManager的authenticate方法中,返回认证结果。
实现UserDetailsService接口
AuthenticationManager的authenticate方法中会遍历执行所有provider的authenticate方法,直到返回结果不为null(为null表示这个provider不适用)。spring security提供了DaoAuthenticationProvider作为默认实现,在这个provider(应该说是所有的provider实现里都调用了UserDetailsService的loadUserByUsername方法,所以需要实现这个接口。
认证结果处理:
编写一个类UsernamePasswordProvider继承DaoAuthenticationProvider,重写createSuccessAuthentication。这个方法会在认证通过后调用,在方法里生成token并返回。
public class UsernamePasswordProvider extends DaoAuthenticationProvider {
final static Integer EXPIRE = 7200;
final static Integer REFRESH_EXPIRE = 30;
final String jwtKey;
final boolean enableExpire;
public UsernamePasswordProvider(String jwtKey,boolean enableExpire) {
this.enableExpire = enableExpire;
this.jwtKey = jwtKey;
}
@Override
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
JwtBuilder refreshBuilder = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, Base64Codec.BASE64URL.encode(jwtKey));
//.compressWith(new GzipCompressionCodec())
JwtBuilder builder = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, Base64Codec.BASE64URL.encode(jwtKey));
//.compressWith(new GzipCompressionCodec())
if(enableExpire) {
builder.setExpiration(Date.from(Instant.now().plusSeconds(EXPIRE)));
refreshBuilder.setExpiration(Date.from(Instant.now().plus(REFRESH_EXPIRE, ChronoUnit.DAYS)));
}
refreshBuilder.setSubject(user.getUsername());
builder.setSubject(user.getUsername());
for(var authorize: user.getAuthorities()){
if(GrantedClaim.class.isAssignableFrom(authorize.getClass())){
GrantedClaim claim = (GrantedClaim)authorize;
builder.claim(claim.getName(),claim.getValue());
}
}
return AccessToken.builder()
.accessToken(builder.compact())
.refreshToken(refreshBuilder.compact())
.expire(enableExpire?EXPIRE:-1)
// .authorities(user.getAuthorities())
.build();
}
}
携带token的api验证
编写一个JwtTokenFilter继承OncePerRequestFilter,重写doFilterInternal方法,在方法里解析token,如果token有问题,抛出异常,否则生成一个自定义的Authentication对象,将对象存入SecurityContext中。
@Slf4j
@Order(100)
public class JwtTokenFilter extends OncePerRequestFilter {
final String jwtKey;
public JwtTokenFilter(String jwtKey) {
this.jwtKey = jwtKey;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String authorization = request.getHeader("Authorization");
if (authorization == null || !authorization.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
String token = authorization.replace("Bearer ", "");
try {
var parse = Jwts.parser()
.setAllowedClockSkewSeconds(300) //允许5分钟容差
.setSigningKey(Base64Codec.BASE64URL.encode(jwtKey))
.parseClaimsJws(token);
Claims claims = parse.getBody();
String uid = claims.getOrDefault("uid",0).toString();
String name = claims.getSubject();
List<String> authorities = (List) claims.get("authority");
List<Claim> claimList = new ArrayList<>();
if(authorities != null){
claimList = authorities.stream()
.map(val -> new Claim("uid", uid, val))
.collect(Collectors.toList());
}
SecurityContextHolder.getContext().setAuthentication(new AuthorizedToken(uid,name, claimList));
request.setAttribute("X-UID",uid);
chain.doFilter(request,response);
}catch (JwtException ex){
log.warn(ex.getMessage());
onUnsuccessfulAuthentication(request,response);
}
}
protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.FORBIDDEN.value());
}
}
配置
编写一个SecurityConfig类,继承WebSecurityConfigurerAdapter,使用@Configuration和@EnableWebSecurity注解。使用@Bean注解标注AuthenticationManager和PasswordEncoder的工厂方法,向容器注入这个类的bean实例。重写configure(AuthenticationManagerBuilder auth)方法,在方法中实例化UsernamePasswordProvider,将它的实例和PasswordEncoder、实现UserDetailsService接口的UserService注入到AuthenticationManagerBuilder中。重写configure(HttpSecurity http)方法,调用HttpSecurity的addFilterBefore方法将JwtTokenFilter注册到UsernamePasswordAuthenticationFilter前。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${security.jwt.signature}")
String jwtKey;
@Value("${security.jwt.expire.enable}")
boolean jwtExpireEnable;
@Autowired
UserDetailsService userService;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder(){
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//auth.authenticationProvider()
// auth.inMemoryAuthentication()
// .passwordEncoder(new BCryptPasswordEncoder())
// .withUser(User.builder().username("vincent").password(new BCryptPasswordEncoder().encode("1234")).roles("ADMIN"));
var usernamePasswordProvider = new UsernamePasswordProvider(jwtKey,jwtExpireEnable);
usernamePasswordProvider.setPasswordEncoder(passwordEncoder());
usernamePasswordProvider.setUserDetailsService(userService);
auth.authenticationProvider(usernamePasswordProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().disable()
.csrf().disable()
.cors()
.and()
.authorizeRequests()
.and()
.addFilterBefore(new JwtTokenFilter(jwtKey), UsernamePasswordAuthenticationFilter.class);
}
}