formLogin的方式,指定登录接口url以及登陆成功后的handler,handler类把token返回给前端
http.formLogin()
.loginProcessingUrl("/api/login")
.successHandler((httpServletRequest, httpServletResponse, authentication) -> {
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
String username = httpServletRequest.getParameter("username");
String password = httpServletRequest.getParameter("password");
log.info("登录成功, 用户名:{}, 密码:{}", username, password);
//设置cookie 24小时过期
HttpSession session = httpServletRequest.getSession();
session.setMaxInactiveInterval(60 * 60 * 24);
httpServletResponse.getWriter().write(JSONUtil.toJsonStr(ResponseResult.success(session.getId())));
})
大致流程:
1、先在 AbstractAuthenticationProcessingFilter
中的requiresAuthentication方法判断当前请求是不是登录请求(SecurityConfiguration
里面配置的formLogin的路径)
2、是登录请求的话,然后在这个类UsernamePasswordAuthenticationFilter
的attemptAuthentication方法里面调用UsernamePasswordAuthenticationToken
的二参构造把username
和password
传到UsernamePasswordAuthenticationToken
对象中(principal就是username,credentials就是password),这个时候还是未认证成功的状态,然后调用authenticate方法进行登录认证
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username, password);
this.getAuthenticationManager().authenticate(authRequest);
3、authenticate方法由子类ProviderManager
来实现,子类根据传递的参数类型选择对应的provider执行
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
result = provider.authenticate(authentication);
}
4、自定义一个provider需要继承DaoAuthenticationProvider
并且实现supports方法(authenticate方法也可以选择自己实现)
不重写authenticate方法的话,最终是调用AbstractUserDetailsAuthenticationProvider
的authenticate方法
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
}
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
5、其中retrieveUser方法调用的是DaoAuthenticationProvider
的方法,最终是调用自己实现好的loadUserByUsername方法(实现UserDetailsService
接口,重写该方法,根据用户名查询用户信息)
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
additionalAuthenticationChecks方法调用的也是DaoAuthenticationProvider
的方法,比对密码的逻辑就在这里(也可以自己重写该方法),密码加密方式需要在SecurityConfiguration
中指定,不指定的话默认使用的是bcrypt
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
6、密码比对成功后将会调用createSuccessAuthentication方法,最终是调用了UsernamePasswordAuthenticationToken
类的三参构造,这时才是认证成功的authentication
对象
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
7、上面是认证逻辑,认证成功之后在attemptAuthentication方法之后把登陆成功的用户信息写入SecurityContext
中
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
8、然后在后面的SecurityContextPersistenceFilter
中,通过这行SecurityContextRepository
.saveContext方法保存session
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
最终是调用的setAttribute方法保存到数据库(JdbcSession)或者内存(MapSession)中去
httpSession.setAttribute(springSecurityContextKey, context);
9、最后调用登录成功后的处理类,把token返回给前端
10、第二次请求过来时,在请求头携带token,请求头的headerName需要自己指定
@Bean
public HttpSessionIdResolver httpSessionIdResolver() {
return new HeaderHttpSessionIdResolver("access-token");
}
在SecurityContextPersistenceFilter
这个过滤器中,调用这行
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
HttpSessionSecurityContextRepository
.loadContext方法
@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
HttpServletRequest request = requestResponseHolder.getRequest();
HttpServletResponse response = requestResponseHolder.getResponse();
HttpSession httpSession = request.getSession(false);
SecurityContext context = readSecurityContextFromSession(httpSession);
if (context == null) {
context = generateNewContext();
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Created %s", context));
}
}
SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(response, request,
httpSession != null, context);
requestResponseHolder.setResponse(wrappedResponse);
requestResponseHolder.setRequest(new SaveToSessionRequestWrapper(request, wrappedResponse));
return context;
}
通过request.getSession方法,根据请求头中的token获得session