经过上面的自定义短信登录认证,想必大家对自定义登录实现已经比较熟悉了。
我们想要实现一个自定义登录组件,无非就分为五步:
记住上面的五个步骤,实现任意的自定义登录就没有问题了,接下来我们就按照上面的顺序,实现jwt方式的登录。
前后端分离项目传递参数一般都使用json方法,不使用默认的表单登录,所以我们需要自定义一个filter,从request中获取用户名密码:
public class JwtAuthenticationLoginFilter extends AbstractAuthenticationProcessingFilter {
public JwtAuthenticationLoginFilter() {
super(new AntPathRequestMatcher("/jwtLogin", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
//从json中获取username和password
String body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
String username = "";
String password = "";
if(StringUtils.hasText(body)) {
JsonObject jsonObject = new JsonParser().parse(body).getAsJsonObject();
username = jsonObject.get("username").getAsString().trim();
password = jsonObject.get("password").getAsString().trim();
}
//封装到token中提交
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
return getAuthenticationManager().authenticate(authRequest);
}
}
其实仔细想一下,我们这里使用的还是用户名密码登录,没有说要用其他参数校验,所以为什么不直接使用系统提供的DaoAuthenticationProvider
呢,所以就不需要再自定义实现了~~(并不是因为懒)~~,直接跳过~
不论是登录成功还是失败,都需要传给前端json格式的数据,而不是像之前一样后端自己做重定向。
如果说用户身份验证通过了,那么我们需要生成一个token放入到header中,所以对于登录成功的回调:
@Slf4j
@Component
public class JwtAuthSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private UserService userService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
User user = (User) authentication.getPrincipal();
String generateToken = userService.generateToken(user);
response.setHeader("jwt-token",generateToken);
log.info("生成token并设置给header: {}",generateToken);
//登录成功将用户信息返回
PrintWriter out = response.getWriter();
user.setPassword("");
String userJson = new Gson().toJson(user, User.class);
out.write(userJson);
out.flush();
out.close();
}
}
登录失败也要返回失败原因:
@Slf4j
@Component
public class JwtAuthFailHandler extends SimpleUrlAuthenticationFailureHandler {
@Data
@AllArgsConstructor
private static class ExceptionResult{
private int code;
private String message;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
log.error("用户登录失败, {}",e.getMessage());
PrintWriter out = resp.getWriter();
ExceptionResult respBean = new ExceptionResult(HttpStatus.UNAUTHORIZED.value(), e.getMessage());
resp.setStatus(HttpStatus.UNAUTHORIZED.value());
if (e instanceof LockedException) {
respBean.setMessage("账户被锁定,请联系管理员!");
} else if (e instanceof CredentialsExpiredException) {
respBean.setMessage("密码过期,请联系管理员!");
} else if (e instanceof AccountExpiredException) {
respBean.setMessage("账户过期,请联系管理员!");
} else if (e instanceof DisabledException) {
respBean.setMessage("账户被禁用,请联系管理员!");
} else if (e instanceof BadCredentialsException) {
respBean.setMessage("用户名或者密码输入错误,请重新输入!");
}
String json = new Gson().toJson(respBean,ExceptionResult.class);
out.write(json);
out.flush();
out.close();
}
}
使用系统提供的DaoAuthenticationProvider
,需要将UserDetailsService
和PasswordEncoder
手动set一下,别问为什么,问就是启动系统会报错:
@Component
public class JwtAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private JwtAuthSuccessHandler jwtAuthSuccessHandler;
@Autowired
private JwtAuthFailHandler jwtAuthFailHandler;
@Autowired
private UserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void configure(HttpSecurity builder) throws Exception {
JwtAuthenticationLoginFilter filter = new JwtAuthenticationLoginFilter();
filter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
filter.setAuthenticationSuccessHandler(jwtAuthSuccessHandler);
filter.setAuthenticationFailureHandler(jwtAuthFailHandler);
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userService);
provider.setPasswordEncoder(passwordEncoder);
builder.authenticationProvider(provider);
builder.addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class);
}
}
当当当当,上面的组件已经搞定了,最后配置一下我们的security:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SmsAuthenticationSecurityConfig smsAuthenticationSecurityConfig;
@Autowired
private JwtAuthenticationSecurityConfig jwtAuthenticationSecurityConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.disable()
.apply(smsAuthenticationSecurityConfig)
.and()
.apply(jwtAuthenticationSecurityConfig)
.and()
// 设置URL的授权
.authorizeRequests()
// 这里需要将登录页面放行
.antMatchers("/login","/verifyCode","/smsLogin","/failure","/jwtLogin")
.permitAll()
// anyRequest() 所有请求 authenticated() 必须被认证
.anyRequest()
.authenticated()
.and()
// 关闭csrf
.csrf().disable();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
现在我们可以启动项目用postman验证一下,登录接口:
登录成功,并且返回用户信息,header也成功添加了:
我们在controller中添加一个测试接口:
@GetMapping("/getResource")
@ResponseBody
public ResponseEntity<String> getResource(){
return ResponseEntity.ok("获取资源成功");
}
这个接口应该是只有用户登录之后才能访问的,但是这个时候你会发现,登录完成之后,不需要携带header也能访问这个接口,并且服务器重启之后就不能访问了。这是什么情况?就这都能写一个bug出来,我果然不适合做开发,回家种地吧。
好在经过各位朋友的~~点赞~~哦不支持,我决定继续坚持下去。为什么会发生这种情况呢,因为spring security的登录信息是保存在session当中的,这显然是和jwt相悖的。所以我们还需要做两件事:
接下来就解决这两个问题吧。
调用HttpSecurity的方法:
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
这样就禁用掉session了。是不是非常的简单呢?
那怎么做到拦截请求,验证是否携带token呢。和登录认证一样的,我们要实现请求校验也通过上面的五个步骤。
我们需要另外一个Filter对这些请求做一个拦截。这个拦截器主要是提取header中的token:
public class JwtAuthenticationRequestFilter extends OncePerRequestFilter {
private final RequestHeaderRequestMatcher requestHeaderRequestMatcher = new RequestHeaderRequestMatcher("jwt-token");
@Autowired
private UserService userService;
@Autowired
private JwtAuthFailHandler failHandler;
@Autowired
private JwtAuthRequestSuccessHandler successHandler;
private AuthenticationManager authenticationManager;
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//如果需要不支持匿名用户的请求没带token,这里放过也没问题,因为SecurityContext中没有认证信息,后面会被权限控制模块拦截
if (!requestHeaderRequestMatcher.matches(request)){
filterChain.doFilter(request,response);
return;
}
String token = request.getHeader("jwt-token");
if (StringUtils.isBlank(token)) {
AuthenticationException failed = new InsufficientAuthenticationException("token is empty");
SecurityContextHolder.clearContext();
failHandler.onAuthenticationFailure(request, response, failed);
return;
}else {
User user = userService.getUserInfoFromToken(token);
JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(user.getAuthorities(),user,token);
Authentication authResult = authenticationManager.authenticate(authenticationToken);
//成功的回调方法
SecurityContextHolder.getContext().setAuthentication(authResult);
successHandler.onAuthenticationSuccess(request, response, authResult);
}
filterChain.doFilter(request,response);
}
}
和登录的filter稍微有点区别,这个filter在这里其实就已经完成了jwt token的校验了,所以我们这里生成的JwtAuthenticationToken
其实已经经过验证了。
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
private Object credentials;
public JwtAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
//初始化完成,但是还未认证
setAuthenticated(false);
}
public JwtAuthenticationToken(Collection<? extends GrantedAuthority> authorities, Object principal, Object credentials) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return credentials;
}
@Override
public Object getPrincipal() {
return principal;
}
}
在这个provider中不用做校验了,直接返回这个authentication就行了:
@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
return authentication;
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.isAssignableFrom(JwtAuthenticationToken.class);
}
}
对于token认证失败而言,无非就是返回401状态认证失败的信息给用户,这个和登录的handler是一样的,所以可以直接复用。对于token认证成功,在继续执行filterchain中的其他filter之前,先要检查这个token是否需要刷新,并且不像登录要返回用户信息,所以要重新定义一个handler:
@Slf4j
@Component
@EnableConfigurationProperties(JwtProperties.class)
public class JwtAuthRequestSuccessHandler implements AuthenticationSuccessHandler {
/**
* 刷新间隔5分钟
*/
private final long tokenRefreshInterval = TimeUnit.MINUTES.toSeconds(5);
@Autowired
private UserService userService;
@Autowired
private JwtProperties jwtProperties;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
User user = (User) authentication.getPrincipal();
String oldToken = (String) authentication.getCredentials();
Date expireDate = JwtUtils.getExpireDate(oldToken, jwtProperties.getPublicKey());
if (shouldTokenRefresh(expireDate)){
//重新生成一个token,重置token失效时间
String generateToken = userService.generateToken(user);
log.debug("重新生成token: {}",generateToken);
response.setHeader("jwt-token",generateToken);
}
}
protected boolean shouldTokenRefresh(Date issueAt){
LocalDateTime issueTime = LocalDateTime.ofInstant(issueAt.toInstant(), ZoneId.systemDefault());
return LocalDateTime.now().minusSeconds(tokenRefreshInterval).isAfter(issueTime);
}
}
上面的认证组件准备就绪,接下来还需要一个配置类:
@Component
public class JwtRequestSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private JwtAuthenticationProvider provider;
@Override
public void configure(HttpSecurity builder) throws Exception {
JwtAuthenticationRequestFilter filter = new JwtAuthenticationRequestFilter();
filter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
//将这个filter加入到spring容器中,这样就能在filter中注入了
postProcess(filter);
builder.authenticationProvider(provider);
builder.addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class);
}
}
因为我们的filter使用了spring容器中的对象,所以要调用postProcess
方法,将filter加入到spring容器中。
和登录一样,将这个配置加入到spring security配置类中:
//先注入
@Autowired
private JwtRequestSecurityConfig jwtRequestSecurityConfig;
//然后在http中添加
http.apply(jwtRequestSecurityConfig);
这样一个~~完整~~的spring security的jwt方式的实现登录认证就完成了,使用上面的那个接口测试也完全没有问题,但是啊,这个项目还是没法正式使用,因为没有解决前后端分离项目中的一个最最基本的问题:跨域。
前后端分离项目必然要解决跨域问题,spring security中添加跨域支持:
@Bean
public CorsFilter corsFilter(){
//1.添加cors配置信息
CorsConfiguration config = new CorsConfiguration();
//.1 允许的域,不要写*
List<String> allowedOrigins = new ArrayList<>();
allowedOrigins.add("http://localhost:8080");
allowedOrigins.forEach(config::addAllowedOrigin);
//.2是否发送cookie信息
config.setAllowCredentials(true);
//.3允许的请求方式
List<String> allowedMethods = new ArrayList<>();
allowedMethods.add("GET");
allowedMethods.add("POST");
allowedMethods.add("OPTIONS");
allowedMethods.add("HEAD");
allowedMethods.forEach(config::addAllowedMethod);
//.4允许的头信息
config.addAllowedHeader("*");
config.addExposedHeader("jwt-token");
//.5添加有效时长
config.setMaxAge(3600L);
//2.添加映射路径
UrlBasedCorsConfigurationSource configurationSource = new UrlBasedCorsConfigurationSource();
configurationSource.registerCorsConfiguration("/**",config);
//3.返回新的CorsFilter
return new CorsFilter(configurationSource);
}
然后在http中添加:
http.cors().and().addFilterAfter(corsFilter(), CorsFilter.class)
补一个完整的配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
//super.configure(http);
http.formLogin()
.disable()
//添加header设置,支持跨域和ajax请求
.cors().and()
.addFilterAfter(corsFilter(), CorsFilter.class)
.apply(smsAuthenticationSecurityConfig)
.and()
.apply(jwtAuthenticationSecurityConfig)
.and()
.apply(jwtRequestSecurityConfig)
.and()
// 设置URL的授权
.authorizeRequests()
// 这里需要将登录页面放行
.antMatchers("/login","/verifyCode","/smsLogin","/failure","/jwtLogin")
.permitAll()
// anyRequest() 所有请求 authenticated() 必须被认证
.anyRequest()
.authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 关闭csrf
.csrf().disable();
}
经过上面的一系列自定义认证,相信大家对Spring security已经比较熟悉了。对于自定义登录实现想必也是信手拈来。
对于jwt token相关的生成和校验由于不是本文的重点,所以这里也没有提及,大家可以去gitee查看所有源码:spring security combat,登录认证也不多说了,下篇文章开始讲授权认证了。
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.lingxiaomz.top/articleContent/?id=105818005766203332
内容来源于网络,如有侵权,请联系作者删除!