SpringSecurity6--认证和授权的原理
一、Spring Security简介
Spring Security是一个基于Spring框架的安全解决方案,提供了认证和授权等安全方面的大服务,包括身份认证和权限处理两大服务。Spring Security的实现依赖于大量的过滤器,采用责任链模式对请求请求不同的过滤处理。在日常使用中,Spring Security已经实现了基于表单的登录认证和授权模式,只需简单的配置,即可做到拿来即用。当然Spring Security也提供了灵活的其他认证入口,通过实现其暴漏出来的接口即可自定义自己的登录认证和授权方法。
- 认证(Authentication):是Spring Security识别当前登录用户身份的主要方式。
- 授权(Authorization):是Spring Security对当前认证用户权限管理的主要方式。
二、Spring Security框架中认证流程中几个非常重要的类
1、 FilterChainProxy
这是一个Spring Security
框架中非常重要的类,它用于维护一组过滤器连,属于Servlet级别的过滤器。所有的请求在进入WEB容器之前都会经过该过滤器,该过滤器会对请求进行不同程度的拦截和处理。FilterChainProxy
根据不同的请求获取对应的过滤器链组,之后按照顺序执行该请求对应的过滤器链组中的每一个过滤器的逻辑对该请求进行处理,直到所有的过滤器都执行完成。FilterChainProxy
实际上是一个Filter
,它的doFilter方法最终会调用自己内部的doFilterInternal
方法。
public class FilterChainProxy extends GenericFilterBean {
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
……省略部分代码……
// 根据请求类型,获取请求下的所有的过滤器
List<Filter> filters = getFilters(firewallRequest);
// 循环执行匹配到该请求下的所有的过滤器
FilterChain reset = (req, res) -> {
// Deactivate path stripping as we exit the security filter chain
firewallRequest.reset();
chain.doFilter(req, res);
};
this.filterChainDecorator.decorate(reset, filters).doFilter(firewallRequest, firewallResponse);
}
}
2、 AbstractAuthenticationProcessingFilter
这个是认证过程中抽出来的抽象类,自定义的过滤器可直接实现这个抽象类自定义过滤方式,UsernamePasswordAuthenticationFilter
就是直接实现的这个抽象类。AbstractAuthenticationProcessingFilter
实际上是一个Filter
,它的doFilter方法最终会调用自己内部的attemptAuthentication
方法。这是一个抽象方法,需要子类实现它,UsernamePasswordAuthenticationFilter
就是直接实现该抽象类做到,实现了attemptAuthentication
方法。
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
// 调用内部的attemptAuthentication方法,该方法是抽象方法。需要子类实现。
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 认证成功之后会调用该方法处理认证成功之后的逻辑。
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed
// 认证失败会调用该方法处理认证失败之后的逻辑。
unsuccessfulAuthentication(request, response, ex);
}
}
}
3、 UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilte
是AbstractAuthenticationProcessingFilter
的直接实现,也是Spring Security表单登录的默认实现过滤器,主要是将表单登录的所需要的username
和password
从request中获取出来封装成还未认证过的UsernamePasswordAuthenticationToken
,它实现了AbstractAuthenticationProcessingFilter
的抽象方法attemptAuthentication
方法,最终将登录逻辑的具体实现委托给AuthenticationManager
去实现,而AuthenticationManager
是一个接口,里面只有一个抽象方法authenticate
,必须由子类实现,而最直接的实现该接口的类就是ProviderManager
。
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 从请求中获取username参数
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
// 从请求中获取password参数
String password = obtainPassword(request);
password = (password != null) ? password : "";
// 构造一个没有完全认证的AuthenticationToken,最主要的是就是用来存放未认证之前的用户信息的。未认证之前,AuthenticationToken里面的authenticated属性为false,认证通过直接会被设置为true。
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
// 实际上就是保存了一下用户的详细信息
setDetails(request, authRequest);
// 调用AuthenticationManager的authenticate方法处理认证逻辑,实际上会调到具体的子类实现上。
return this.getAuthenticationManager().authenticate(authRequest);
}
}
4、AbstractAuthenticationToken
AbstractAuthenticationToken
是一个用来存放用户信息的抽象类,比如表单登录的username
和password
就会被存放在该类的实现中,而它的直接实现就是UsernamePasswordAuthenticationToken
。AbstractAuthenticationToken
里面有几个重要的属性:
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {
// 当前登录用户的权限集合
private final Collection<GrantedAuthority> authorities;
// 用户信息的详细信息
private Object details;
// 当前登录用户是否完全认证。 未认证之前,authenticated属性为false,认证通过直接会被设置为true。
private boolean authenticated = false;
}
5、 UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken
是 AbstractAuthenticationToken
的直接实现,也是Spring Security表单登录的默认实现用来存放用户信息的类。该类的作用就是存放未认证之前的用户信息,也就是表单提交的username和password。
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
// 在父类的基础之上,UsernamePasswordAuthenticationToken增加了该属性信息,源码实现的时候,未认证之前该属性代表的是username,认证之后存放的是UserDetails
private final Object principal;
// 在父类的基础之上,UsernamePasswordAuthenticationToken增加了该属性信息,源码实现的时候,未认证之前该属性代表的是password,认证之后存放的是认证主体Authentication的一些信息
private Object credentials;
}
6、 AuthenticationManager
AuthenticationManager
是实际上用来做认证的接口,内部就一个authenticate
抽象方法,子类必须实现它,ProviderManager
是AuthenticationManager
的最直接的实现。
public interface AuthenticationManager {
// 实际上的认证入口
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
7、 ProviderManager
ProviderManager
是AuthenticationManager
的最直接的实现,实现了authenticate
抽象方法,也是Spring Security表单登录的默认实现认证相关的类,认证的处理逻辑就是在这个里面处理的。而在 ProviderManager
又把认证逻辑委托给了对应的AbstractUserDetailsAuthenticationProvider
,他是实际上干活的类,认证逻辑最终在这里执行。
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
// 循环ProviderManager管理的provider,找到那个与之相匹配的provider
for (AuthenticationProvider provider : getProviders()) {
// 找到那个与之相匹配的provider,比如表单登录的ProviderManager仅支持DaoAuthenticationProvider
if (!provider.supports(toTest)) {
continue;
}
try {
// 将认证信息委托给对应的p进行处理,默认是DaoAuthenticationProvider
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
// 假设子类provider认证失败了,会继续使用父类的provider进行认证。但是实际上对于表单登录来说,还是ProviderManager
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
throw lastException;
}
}
8、AbstractUserDetailsAuthenticationProvider
AbstractUserDetailsAuthenticationProvider
是实际上将认证所需要的信息进行收集的抽象类,它重写了authenticate
方法,它主要就是根据request中的username
从数据库中的获取到对应的用户信息,该操作是交给UserDetailsService
接口来实现的,子类需要实现它,并且实现loadUserByUsername
方法,并将用户信息封装成UserDetails对象。最终调用它的直接实现类DaoAuthenticationProvider
进行处理,并将将从数据库中查询出来的用户信息UserDetails
对象和从request中获取的用户信息UsernamePasswordAuthenticationToken
传给该实现类的additionalAuthenticationChecks
方法进行比对,主要是比对的两个用户信息的密码是否匹配,如果匹配就说明认证成功,否则,认证失败。
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean, MessageSourceAware {
@Override
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 {
// 这个方法最终会调用UserDetailsService的loadUserByUsername方法,最终根据username获取用户信息。具体获取用户信息的方式依据子类的实现
user = retrieveUser(username, (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);
// 这个方法最终会调用DaoAuthenticationProvider的additionalAuthenticationChecks方法进行密码比对,如果匹配就说明认证成功,否则,认证失败。
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
// 认证如果能走到这一步,那就说明认证成功了。该方法最终返回一个认证过的UsernamePasswordAuthenticationToken对象,其authenticated的属性的值为true
return createSuccessAuthentication(principalToReturn, authentication, user);
}
}
9、 UserDetailsService
UserDetailsService
是一个接口,主要是提供一个入口,让子类实现根据username
从获取到对应的用户信息的方式,一般情况都是根据username
从数据库中获取用户信息。
public interface UserDetailsService {
/**
* Locates the user based on the username. In the actual implementation, the search
* may possibly be case sensitive, or case insensitive depending on how the
* implementation instance is configured. In this case, the <code>UserDetails</code>
* object that comes back may have a username that is of a different case than what
* was actually requested..
* @param username the username identifying the user whose data is required.
* @return a fully populated user record (never <code>null</code>)
* @throws UsernameNotFoundException if the user could not be found or the user has no
* GrantedAuthority
*/
// 子类必须实现该方法,实现根据username获取用户信息的逻辑
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
10、 DaoAuthenticationProvider
DaoAuthenticationProvider
是AbstractUserDetailsAuthenticationProvider
的直接实现,也是spring security表单登录种密码匹配的默认实现。它匹配密码的方式实际就是调用了PasswordEncoder
的matches
方法进行匹配,如果匹配,登录认证成功,否则失败。
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Override
@SuppressWarnings("deprecation")
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();
// 直接调用PasswordEncoder的matches方法进行密码匹配
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"));
}
}
}
11、AuthenticationSuccessHandler
AuthenticationSuccessHandler
是认证成功处理类,是一个接口,需要子类去显示它,并实现onAuthenticationSuccess
方法,通过实现该方法,可以自定义返回结果。比如前后端分离项目,后端认证成功之后,需要返回一个JSON格式的数据给前台,那么就可以实现AuthenticationSuccessHandler
的onAuthenticationSuccess
方法处理。
public interface AuthenticationSuccessHandler {
void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException;
}
12、AuthenticationFailureHandler
AuthenticationFailureHandler
是认证失败处理类,是一个接口,需要子类去显示它,并实现onAuthenticationFailure
方法,通过实现该方法,可以自定义返回结果。比如前后端分离项目,后端认证失败之后,需要返回一个JSON格式的数据给前台,那么就可以实现AuthenticationFailureHandler
的onAuthenticationFailure
方法处理。
public interface AuthenticationFailureHandler {
void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException;
}
三、Spring Security框架认证流程
![[Spring Security认证流程.png]]
四、编写新的认证方式
1、编写新的认证方式的步骤
① 构建一个新的AuthenticationFilter
业务类,继承AbstractAuthenticationProcessingFilter
抽象类,可参照UsernamePasswordAuthenticationFilter
。
② 构建一个新的AuthenticationToken
业务类,实现AbstractAuthenticationToken
抽象类,可参照UsernamePasswordAuthenticationToken
。
③ 构建一个AuthenticationManager
业务类,实现AuthenticationManager
接口,实现authenticate
方法,可参照ProviderManager
,也可以直接使用ProviderManager
,只是需要将自定义的AuthenticationProvider
指定到该ProviderManager
对象当中。
③ 构建一个AuthenticationProvider
业务类,实现AuthenticationProvider
接口,实现authenticate
方法,可参照DaoAuthenticationProvider
。
④ 构建两个Handle
,一个是认证成功之后的处理器,实现AuthenticatonSuccessHandler
接口,重写onAuthenticationSuccess
方法,处理认证成功之后的逻辑处理,一个是认证失败之后的处理,实现AuthenticationFailureHandler
接口,重写onAuthenticationFailure
方法,处理认证失败之后的逻辑。
⑤ 构建一个UserDetailsService
,实现UserDetailsService
接口,实现loadUserByUsername
方法,自定义加载用户信息的逻辑,可参照CachingUserDetailsService
。
⑥ 构建一个UserDetails
,实现UserDetails
接口,自定义用户信息属性。
⑦ 构建JWT存储库,用来存储token。
⑧ 构建一个AuthenticationFilterConfigurer
配置类,实现AuthenticationFilterConfigurer
接口,自定义配置新的认证方式。
⑨ 构建配置类,注入SecurityFilterChain
类。
2、编写新的认证方式----短信登录("/codeLogin")
① 构建一个新的OrkasgbSMSAuthencationFilter
业务类,继承AbstractAuthenticationProcessingFilter
抽象类,可参照UsernamePasswordAuthenticationFilter
。
/**
* OrkasgbSMSAuthencationFilter类在启用了Spring Security的应用程序中实现了一个定制的基于SMS的身份验证过滤器。
* 它检查带有所需参数的POST请求,创建身份验证令牌,如果成功,则返回经过身份验证的用户对象。
*
* @date Sep 21, 2010 4:21:21 PM
* @since 0.0.5
*/
@Component
public class OrkasgbSMSAuthencationFilter extends AbstractAuthenticationProcessingFilter {
/**
* 自定义请求路经为"/codeLogin",请求方式为POST
*/
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/codeLogin",
"POST");
/**
* 构造函数,构造是时需要做以下操作:
* <ol>
* 1. 设置需要身份验证的请求路径为DEFAULT_ANT_PATH_REQUEST_MATCHER;
* 2. 设置身份验证过滤器的匹配器,因为使用的还是默认的ProviderManager实现,所有需要手动将Provider替换为自定义的
* OrkasgbSMSAuthenticationProvider;
* </ol>
*/
public OrkasgbSMSAuthencationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
OrkasgbSMSAuthenticationProvider orkasgbSMSAuthenticationProvider =
new OrkasgbSMSAuthenticationProvider();
ProviderManager providerManager = new ProviderManager(orkasgbSMSAuthenticationProvider);
// 这里必须调用手动设置下providerManager,否则会使用默认的,那么最终几不会走到自定义的SMS身份验证过滤器的代码块中。
this.setAuthenticationManager(providerManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String phoneNumber = request.getParameter("phoneNumber");
String code = request.getParameter("code");
// 此处构造的是一个还未认证的OrkasgbSMSAuthenticationToken,即里面的authenticated属性为false。
OrkasgbSMSAuthenticationToken orkasgbSMSAuthenticationToken = new OrkasgbSMSAuthenticationToken(phoneNumber, code);
setDetails(request, orkasgbSMSAuthenticationToken);
// 认证方式委托为providerManager,providerManager又会委托给orkasgbSMSAuthenticationProvider来执行身份验证。
return this.getAuthenticationManager().authenticate(orkasgbSMSAuthenticationToken);
}
@Override
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
super.setAuthenticationManager(authenticationManager);
}
protected void setDetails(HttpServletRequest request, OrkasgbSMSAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
@Override
public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
super.setSecurityContextRepository(securityContextRepository);
}
}
② 构建一个新的OrkasgbSMSAuthenticationToken
业务类,实现AbstractAuthenticationToken
抽象类,可参照UsernamePasswordAuthenticationToken
。
public class OrkasgbSMSAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = -8447558989998984221L;
private final Object principal;
private final Object credentials;
public OrkasgbSMSAuthenticationToken(Object phoneNumber, Object code) {
super(null);
this.credentials = phoneNumber;
this.principal = code;
//
setAuthenticated(false);
}
public OrkasgbSMSAuthenticationToken(Object phoneNumber, Object userDetails, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.credentials = phoneNumber;
this.principal = userDetails;
super.setAuthenticated(true); // must use super, as authorities must be set
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted");
}
super.setAuthenticated(false);
}
@Override
public String toString() {
return getClass().getSimpleName() + "[principal=" + principal + ", authenticated=" + isAuthenticated() + "]";
}
}
③ 构建一个AuthenticationManager
业务类,实现AuthenticationManager
接口,实现authenticate
方法,可参照ProviderManager
,也可以直接使用ProviderManager
,只是需要将自定义的AuthenticationProvider
指定到该ProviderManager
对象当中。
OrkasgbSMSAuthenticationProvider orkasgbSMSAuthenticationProvider =
new OrkasgbSMSAuthenticationProvider();
ProviderManager providerManager = new ProviderManager(orkasgbSMSAuthenticationProvider);
// 这里必须调用手动设置下providerManager,否则会使用默认的,那么最终几不会走到自定义的SMS身份验证过滤器的代码块中。
this.setAuthenticationManager(providerManager);
③ 构建一个OrkasgbSMSAuthenticationProvider
业务类,实现AuthenticationProvider
接口,实现authenticate
方法,可参照DaoAuthenticationProvider
。
/**
* 真正认证的处理类,具体实现了根据短信验证码进行身份验证的功能。主要是通过手机号码获取到验证码,进行匹配。
*
* @date
* @since 1.0.0
*/
@Component
public class OrkasgbSMSAuthenticationProvider implements AuthenticationProvider {
private UserCache userCache = new NullUserCache();
private final OrkasgbSMSUserDetailsService orkasgbSMSUserDetailsService;
/**
* 构造的时候将实际上获取用户信息的实现类注入
*/
public OrkasgbSMSAuthenticationProvider() {
this.orkasgbSMSUserDetailsService = new OrkasgbSMSUserDetailsService();
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OrkasgbSMSAuthenticationToken orkasgbSMSAuthenticationToken = (OrkasgbSMSAuthenticationToken) authentication;
String phoneNumber = (String) orkasgbSMSAuthenticationToken.getCredentials();
UserDetails orkasgbSMSUserDetails = this.orkasgbSMSUserDetailsService.loadUserByUsername(phoneNumber);
if (Objects.isNull(orkasgbSMSUserDetails)) {
throw new IllegalStateException("User not found");
}
// 这里的验证码实际上是从前端传过来的,而实际上保存在后台的验证码是通过orkasgbSMSUserDetailsService从其他存储介质中获取的,
// 实际上就是前后端的验证做一个匹配验证,匹配不通过将会抛出异常。
String code = (String) orkasgbSMSAuthenticationToken.getPrincipal();
if (!StringUtils.equals(code, orkasgbSMSUserDetails.getPassword())) {
throw new BadCredentialsException("Bad credentials");
}
// 验证通过之后,就会构造一个新的Authentication对象,这个是完全认证过的对象,里面有用户的授权信息,并且里面的authenticated属性为true。
OrkasgbSMSAuthenticationToken orkasgbSMSAuthenticationedToken =
new OrkasgbSMSAuthenticationToken(phoneNumber, orkasgbSMSUserDetails, orkasgbSMSUserDetails.getAuthorities());
orkasgbSMSAuthenticationedToken.setDetails(orkasgbSMSUserDetails);
SecurityContextHolder.getContext().setAuthentication(orkasgbSMSAuthenticationedToken);
return orkasgbSMSAuthenticationedToken;
}
@Override
public boolean supports(Class<?> authentication) {
return OrkasgbSMSAuthenticationToken.class.isAssignableFrom(authentication);
}
}
④ 构建两个Handle
,一个是认证成功之后的处理器,实现AuthenticatonSuccessHandler
接口,重写onAuthenticationSuccess
方法,处理认证成功之后的逻辑处理,一个是认证失败之后的处理,实现AuthenticationFailureHandler
接口,重写onAuthenticationFailure
方法,处理认证失败之后的逻辑。
/**
* 自定义成功处理器,用于处理登录成功的情况。
* <p>
* 仅将成功消息提供给前端,不受其他现有处理器的影响。
*/
@Component("orkasgbSuccessHandle")
public class OrkasgbSuccessHandle implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.displayName());
response.setHeader(HttpHeaders.AUTHORIZATION, request.getAttribute("accessToken").toString());
response.getWriter().write(CommonResult.ofSuccess(authentication, "登录成功!").toString());
}
}
/**
* 自定义失败处理器,用于处理登录失败的情况。
* <p>
* 仅将错误消息提供给前段,不受其他现有处理器的影响。
*/
@Component("orkasgbFailureHandler")
public class OrkasgbFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.displayName());
System.out.println("认证失败" + exception.getMessage());
response.getWriter().write(CommonResult.ofFailure("认证失败," + exception.getMessage()).toString());
}
}
⑤ 构建一个UserDetailsService
,实现UserDetailsService
接口,实现loadUserByUsername
方法,自定义加载用户信息的逻辑,可参照CachingUserDetailsService
。
/**
* 用于检查用户名和密码是否正确的服务器。实现自己的UserDetailService接口,并实现这个接口的方法。主要是自定义方法来返回用户信息。
*
* @date
* @since
*/
@Component("orkasgbSMSUserDetailsService")
public class OrkasgbSMSUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String phoneNumber) throws UsernameNotFoundException {
// 模拟从其他介质中查询验证码
String code = this.getCodeByPhoneNumber(phoneNumber);// throw exception if not found
List<SimpleGrantedAuthority> grantedAuthorities = List.of(new SimpleGrantedAuthority("ADMIN2"));
// 构造userDetails
return new OrkasgbSMSUserDetails(phoneNumber, code, grantedAuthorities);
}
private String getCodeByPhoneNumber(String phoneNumber) {
return "239802";
}
}
⑥ 构建一个UserDetails
,实现UserDetails
接口,自定义用户信息属性。
/**
* 短信登录用户详细信息主体,用于存储用户信息的类。
*
* @date Sep 6, 2017 7:21:40 PM
* @since 0.0.6
*/
@Data
public class OrkasgbSMSUserDetails implements UserDetails {
private String phoneNumber;
private String code;
private final Set<GrantedAuthority> authorities;
public OrkasgbSMSUserDetails(String phoneNumber, String code, Collection<? extends GrantedAuthority> authorities) {
this.phoneNumber = phoneNumber;
this.code = code;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.code;
}
@Override
public String getUsername() {
return this.phoneNumber;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
private static SortedSet<GrantedAuthority> sortAuthorities(Collection<? extends GrantedAuthority> authorities) {
Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection");
// Ensure array iteration order is predictable (as per
// UserDetails.getAuthorities() contract and SEC-717)
SortedSet<GrantedAuthority> sortedAuthorities = new TreeSet<>(
Comparator.comparing(GrantedAuthority::getAuthority));
for (GrantedAuthority grantedAuthority : authorities) {
Assert.notNull(grantedAuthority, "GrantedAuthority list cannot contain any null elements");
sortedAuthorities.add(grantedAuthority);
}
return sortedAuthorities;
}
}
⑦ 构建JWT存储库,用来存储token。
/**
* 自定义JWT生成器,用于生成JWT和验证JWT。
*
* @date
* @since
*/
@Component
public class OrkasgbJWTSecurityContextRepository implements SecurityContextRepository {
private static final SecretKey ORKASGB_AUTHENTICATINO_JWT_KEY =
new SecretKeySpec("ORKASGB_AUTHENTICATINO_JWT".getBytes(StandardCharsets.UTF_8), "AES");;
private static final String ORKASGB_AUTHENTICATINO_TOKEN_KEY = "ORKASGB:AUTHENTICATION:";
private static final JwtParser JWT_PARSER = Jwts.parser().setSigningKey(ORKASGB_AUTHENTICATINO_JWT_KEY);
private static final long ORKASGB_AUTHENTICATION_EXPIRE_IN_MS = Duration.ofMinutes(2).toMillis();
private static final Map<String, Object> JWT_GRANTED_AUTHORITY_MAP = new HashMap<>(256);
@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
HttpServletRequest request = requestResponseHolder.getRequest();
String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
SecurityContext context = SecurityContextHolder.createEmptyContext();
if(StringUtils.isBlank(authorization)){
return context;
}
Jws<Claims> claimsJws;
try {
// 解析JWT
claimsJws = JWT_PARSER.parseClaimsJws(authorization);
}catch (Exception e) {
return context;
}
Claims claims = claimsJws.getBody();
String subject = claims.getSubject();
UserDetails userDetails = (UserDetails) JWT_GRANTED_AUTHORITY_MAP.get("authorities" + subject);
Collection<GrantedAuthority> grantedAuthorityList = new ArrayList<>(userDetails.getAuthorities());
OrkasgbJWTSecurityContextToken orkasgbJWTSecurityContextToken =
new OrkasgbJWTSecurityContextToken(grantedAuthorityList, grantedAuthorityList, subject);
context.setAuthentication(orkasgbJWTSecurityContextToken);
return context;
}
@Override
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
UserDetails userDetails = (UserDetails) context.getAuthentication().getPrincipal();
JWT_GRANTED_AUTHORITY_MAP.put("authorities" + userDetails.getUsername(), userDetails);
String token = Jwts.builder()
.setId(userDetails.getUsername().concat("_").concat(UUID.randomUUID().toString()))
.setSubject(userDetails.getUsername()) // 发行者
.setIssuer(userDetails.getUsername()) // 发行者
.setIssuedAt(Calendar.getInstance().getTime()) // 发行时间
.signWith(SignatureAlgorithm.HS256, ORKASGB_AUTHENTICATINO_JWT_KEY) // 签名类型 与 密钥
.compressWith(CompressionCodecs.DEFLATE) // 对载荷进行压缩
.setExpiration(new Date(System.currentTimeMillis() + ORKASGB_AUTHENTICATION_EXPIRE_IN_MS))
.compact();
response.setHeader(ORKASGB_AUTHENTICATINO_TOKEN_KEY, token);
request.setAttribute("accessToken", token);
}
@Override
public boolean containsContext(HttpServletRequest request) {
String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
if(StringUtils.isBlank(authorization)) {
return false;
}
try {
JWT_PARSER.parseClaimsJws(authorization);
}catch (Exception e) {
return false;
}
return true;
}
}
⑦ 构建一个AuthenticationFilterConfigurer
配置类,实现AuthenticationFilterConfigurer
接口,自定义配置新的认证方式。
/**
* 自定义配置,主要是配置新的认证方法
*
* @date
* @since
*/
public class OrkasgbSMSSecurityConfig<H extends HttpSecurityBuilder<H>> extends
AbstractAuthenticationFilterConfigurer<H, OrkasgbSMSSecurityConfig<H>, OrkasgbSMSAuthencationFilter> {
private final OrkasgbSMSAuthenticationProvider orkasgbSMSAuthenicationProvider;
private final OrkasgbSuccessHandle orkasgbSuccessHandle;
private final OrkasgbFailureHandler orkasgbFailureHandler;
private final OrkasgbJWTSecurityContextRepository orkasgbJWTSecurityContextRepository;
public OrkasgbSMSSecurityConfig() {
super(new OrkasgbSMSAuthencationFilter(), "/codeLogin");
this.orkasgbSMSAuthenicationProvider = new OrkasgbSMSAuthenticationProvider();
this.orkasgbFailureHandler = new OrkasgbFailureHandler();
this.orkasgbSuccessHandle = new OrkasgbSuccessHandle();
this.orkasgbJWTSecurityContextRepository = new OrkasgbJWTSecurityContextRepository();
}
@Override
protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
return new AntPathRequestMatcher("/codeLogin", "POST");
}
@Override
public void configure(H http) throws Exception {
OrkasgbSMSAuthencationFilter orkasgbSMSAuthenticationFilter = new OrkasgbSMSAuthencationFilter();
// 设置 AuthenticationManager
orkasgbSMSAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
// 分别设置成功和失败处理器
orkasgbSMSAuthenticationFilter.setAuthenticationSuccessHandler(orkasgbSuccessHandle);
orkasgbSMSAuthenticationFilter.setAuthenticationFailureHandler(orkasgbFailureHandler);
orkasgbSMSAuthenticationFilter.setSecurityContextRepository(orkasgbJWTSecurityContextRepository);
// 创建 SmsCodeAuthenticationProvider 并设置 userDetailsService
// 将Provider添加到其中
http
.authenticationProvider(this.orkasgbSMSAuthenicationProvider)
// 将过滤器添加到UsernamePasswordAuthenticationFilter后面
.addFilterBefore(orkasgbSMSAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
super.configure(http);
}
}
⑧ 构建配置类,注入SecurityFilterChain
类。
/**
* spring security自动化配置。
*/
@Configuration
@EnableAutoConfiguration
@EnableMethodSecurity
@EnableWebSecurity
public class OrkasgbSecurityConfig {
@Autowired
private OrkasgbUserDetailsService orkasgbUserDetailsService;
@Autowired
private OrkasgbSuccessHandle orkasgbSuccessHandle;
@Autowired
private OrkasgbFailureHandler orkasgbFailureHandler;
@Autowired
private OrkasgbJWTSecurityContextRepository orkasgbJWTSecurityContextRepository;
@Autowired
private OrkasgbSMSUserDetailsService orkasgbSMSUserDetailsService;
/**
* 配置 SecurityFilterChain
*
* @param httpSecurity HttpSecurity实例
* @return HttpSecurity实例
* @throws Exception 异常
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 配置表单登录
.formLogin()
// 配置处理登录请求的URL
.loginProcessingUrl( "/login")
// 配置请求参数,获取用户名username
.usernameParameter("username")
// 配置请求参数,获取密码password
.passwordParameter("password")
// 配置处理成功的回调
.successHandler(orkasgbSuccessHandle)
// 配置处理失败的回调
.failureHandler(orkasgbFailureHandler)
.securityContextRepository(orkasgbJWTSecurityContextRepository)
.and()
.securityContext()
.securityContextRepository(orkasgbJWTSecurityContextRepository)
.and()
.cors()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 配置自定义UserDetailsService
.userDetailsService(orkasgbUserDetailsService)
// 禁止csrf跨站请求伪造
.csrf()
.disable()
// 所有的请求都需求要认证
.authorizeHttpRequests()
.requestMatchers("/login", "/loginOut")
.permitAll()
// 所有的请求都需求要认证
.anyRequest()
// 所有的请求都需求要认证
.authenticated()
.and()
// 异常处理器
.exceptionHandling();
//.authenticationEntryPoint(authenticationEntryPoint);
// 新的认证方式
OrkasgbSMSSecurityConfig<HttpSecurity> orkasgbSMSSecurityConfig =
new OrkasgbSMSSecurityConfig<>();
orkasgbSMSSecurityConfig.securityContextRepository(orkasgbJWTSecurityContextRepository);
orkasgbSMSSecurityConfig.loginProcessingUrl("/codeLogin").permitAll();
// 调用此方法将新的认证方式配置进去。
httpSecurity.apply(orkasgbSMSSecurityConfig);
return httpSecurity.build();
}
}