Foreword
The company recently made a new project, using the spring security framework for authentication and authorization. It took three days to complete:
- Account password
- jwt
- url-level access
In fact, before a project it is also used, but basically ctrl + c, ctrl + v. Not too understand the overall framework of this process, it is foggy, but actually ran quite normal.
I have seen some online tutorials before, but did not look too understand, because they though about the most important things are talked about, and some less important, but it can nexus point not mentioned, so see or relatively ignorant, typically I understand, but I want to write, I will not! So in Sidongfeidong state, he embarked on this road of no return.
Ado, get to the point! ! !
Pre-knowledge
spring security is achieved through a series of function corresponding to a filter, each filter corresponds with one function. So in this case the order of the filter is very important, because each filter are likely to modify the original data, or only through a filter to filter the request to the next filter. This is the spring of the core principles of security. Ye looks like pretty simple.
Filter chain
This is an entire filter chain I used in the project, where 6,7 is the custom.6: is a filter password account, 7: is a filter for verification jwt, 12: check permission for a filter
Account password
Account password is almost all projects would have for a function.
Filter Code
public class CustomUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
CustomUsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
String body = StreamUtils.copyToString(httpServletRequest.getInputStream(), Charset.forName("UTF-8"));
String username = null;
String password = null;
if (StringUtils.hasText(body)){
JSONObject jsonObject = JSON.parseObject(body);
username = jsonObject.getString("username");
password = jsonObject.getString("password");
}
if (username == null){
username = "";
}
if (password == null){
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
复制代码
Let's analyze the code:
First, we inherit an abstract class filter, the filter class is actually an ordinary filter, but he did some things in it, and we do not need to know these things specifically used to doing, as long as we it contains a known doFilter
method
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authResult);
}
复制代码
We can see where the implementation of attemptAuthentication
this method, the name of this method is not the way we like from the filter definitions rewrite the name of it, yes, exactly, and it is our custom filters here actually calls inside overridden attemptAuthentication
method
I explain here is actually used a template pattern, template mode is actually most common features have been implemented by the parent, while some special features can be delayed to subclasses to achieve. But the process is still calling the parent to decide, but the specific sub-class implements a single function processes only. In java, there are many places to use this design pattern, such as the famous abstract concurrency control for synchronous queue AbstractQueuedSynchronized , is to use a template model, we are interested can look at the source code, written especially subtle.
I haircut whole process: First, the filter chain will perform to CustomUsernamePasswordAuthenticationFilter
the parent of the filter doFilter
method, and doFilter
the method will be executed overridden attemptAuthentication
method.
Then we enter into the attemptAuthentication
process, look at this method are done:
First, acquired body
in the username and password, and a configuration UsernamePasswordAuthenticationToken
object to the next method.
Focus here !!!
It constructed object passed on, then it is passed to who?
First, let's look at getAuthenticationManager()
this method is doing
protected AuthenticationManager getAuthenticationManager() {
return authenticationManager;
}
复制代码
Actually returned a certified manager, but the manager of this certification, what is?
Then we enter into the AuthenticationManager
inside, which is found in an interface only. Since it is an interface, it certainly can not be instantiated, but here it instantiates an AuthenticationManager
object, the truth is only one, spring security provides a default class that implements the interface. Yes, the fact is that this is ProviderManager
the class.
We first look at his source
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
// ~ Static fields/initializers
// =====================================================================================
private static final Log logger = LogFactory.getLog(ProviderManager.class);
// ~ Instance fields
// ================================================================================================
private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
private List<AuthenticationProvider> providers = Collections.emptyList();
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private AuthenticationManager parent;
private boolean eraseCredentialsAfterAuthentication = true;
public ProviderManager(List<AuthenticationProvider> providers) {
this(providers, null);
}
public ProviderManager(List<AuthenticationProvider> providers,
AuthenticationManager parent) {
Assert.notNull(providers, "providers list cannot be null");
this.providers = providers;
this.parent = parent;
checkState();
}
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;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// 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 e) {
lastException = parentException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
}
复制代码
I put some irrelevant comments and code are removed, leaving only the useful part, or too long, not too good-looking.
First, we found that there is a authenticate
method, the name of this method is not with us CustomUsernamePasswordAuthenticationFilter
like the last line of code inside the same, yes, that is to call this method here.
This method is mainly used to manage Provider
, in fact, is called here the various Provider
methods used to verify that all of Provider are realized AuthenticationProvider
this interface.
We mainly see is provider.authenticate(authentication)
this statement, he is really used method validation. The real validation process is to achieve by our own, or use the default.
And we realize account password to be used Provider
is already provided by the spring security to us DaoAuthenticationProvider
, then here it is a template mode (this mode really quite common ah)!
Together we analyze this class in the end to do what?
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
/**
* The plaintext password used to perform
* PasswordEncoder#matches(CharSequence, String)} on when the user is
* not found to avoid SEC-2056.
*/
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
// ~ Instance fields
// ================================================================================================
private PasswordEncoder passwordEncoder;
/**
* The password used to perform
* {@link PasswordEncoder#matches(CharSequence, String)} on when the user is
* not found to avoid SEC-2056. This is necessary, because some
* {@link PasswordEncoder} implementations will short circuit if the password is not
* in a valid format.
*/
private volatile String userNotFoundEncodedPassword;
private UserDetailsService userDetailsService;
private UserDetailsPasswordService userDetailsPasswordService;
public DaoAuthenticationProvider() {
setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
// ~ Methods
// ========================================================================================================
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
this.passwordEncoder = passwordEncoder;
this.userNotFoundEncodedPassword = null;
}
protected PasswordEncoder getPasswordEncoder() {
return passwordEncoder;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
protected UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsPasswordService(
UserDetailsPasswordService userDetailsPasswordService) {
this.userDetailsPasswordService = userDetailsPasswordService;
}
}
复制代码
AbstractUserDetailsAuthenticationProvider
This class is truly AuthenticationProvider
the abstract class interface.
The abstract class is a template pattern in the parent class is a subclassDaoAuthenticationProvider
Abstract class just defines the specific process, the real work is to verify the implementation by subclasses to concrete implementation.
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// 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);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
复制代码
The more important is the retrieveUser
method that is user access to information systems, and here is the account password, and then put them into a constructed UserDetails
object.
The calibration method is additionalAuthenticationChecks
that the method accepts two parameters, one is UsernamePasswordAuthenticationToken
, the other is UserDetails
to explain before through knowledge we know, UsernamePasswordAuthenticationToken
is the object of the request by the user login account, password structure. UserDetails
Is the object of accounts, passwords construction of the system. As long as we compare these two objects are the same information, we can know whether the user can log on.
After successful validation will call a createSuccessAuthentication
method that calls a callback successful, then we can return token to the user in the callback.
If the validation fails, also called a failed method, the principle is the same with success, but returns information to the user's information is wrong.
The success and failure callback we also need to customize. Success is achieved through AuthenticationSuccessHandler
the interface, failure is by implementing the AuthenticationFailureHandler
interface.
We know that retrieveUser
the method will obtain user information in the system, then it is how to obtain it?
He will execute this statementthis.getUserDetailsService().loadUserByUsername(username);
getUserDetailsService()
This method with the Service on our business is the same, but spring security requires us to realize that they provide UserDetailsService
an interface only one method loadUserByUsername
that accepts a username
parameter, we are obtaining information about the user in the process. Because DAO, so I came to get the user information by querying the database username. Then a configured UserDetails
object is returned.
Of course, this UserDetails
is also an interface, we need to implement this interface, a custom UserDetails
implementation class to use.
Finally, we just pass a Configurer object all the custom components combine it.
package com.liangxin.airport.security;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
/**
* 登录校验的配置,就是将各个有关的类进行组合,让spring security可以识别的出来
*
* @author LHD
* @date 2019/12/9 17:17
*/
public class JsonLoginConfigurer<T extends JsonLoginConfigurer<T, B>, B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<T, B> {
private CustomUsernamePasswordAuthenticationFilter authFilter;
public JsonLoginConfigurer(){
this.authFilter = new CustomUsernamePasswordAuthenticationFilter();
}
@Override
public void configure(B http){
// 设置filter使用AuthenticationManager
authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
// 设置失败的handler
authFilter.setAuthenticationFailureHandler(new JsonLoginFailureHandler());
// 不将认证后的context放入session
authFilter.setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy());
CustomUsernamePasswordAuthenticationFilter filter = postProcess(authFilter);
http.addFilterAfter(filter, LogoutFilter.class);
}
public JsonLoginConfigurer<T, B> loginSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler){
authFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
return this;
}
}
复制代码
to sum up
Because the source code used in a lot of interfaces, inheritance, design patterns, so looks like it would be more around.
I'm here to summarize the whole process:
Filter
-> ProviderManager
-> AbstractUserDetailsAuthenticationProvider
-> DaoAuthenticationProvider
-> AuthenticationSuccessHandler
Filter configured first enters a token information provided by the user (for later verify), then ProviderManager
proceeds to the corresponding Provider
, then Provider
acquired by the username information to the user in the system, then the user with the system token before the compare information on the call is successful AuthenticationSuccessHandler
, it fails to call AuthenticationFailureHandler
.
end
Here we put the entire login process finished, because the company's projects, there is a confidentiality agreement, all of the source code can not be directly attached to it, but if you still do not know where you can leave a message at any time, I will try to answer.