Why do you want to walk through the Spring Security login process with everyone? This is because of a question asked by my friends: How to dynamically modify user information in Spring Security?
If you figure out the Spring Security login process, this is actually not a problem.
First, let's briefly describe the problem scenario:
You use Spring Security for the security management of the server. After the user logs in successfully, Spring Security will help you save the user information in the Session, but you may not know where it is, if you don’t go into it, this brings a problem. If the user modifies the current user information in the front-end operation, how can I obtain the latest user information without logging in again? This is the question to be introduced to the building today.
Article Directory
1. Ubiquitous Authentication
Friends who have played Spring Security know that there is a very important object in Spring Security called Authentication. We can inject Authentication anywhere to get the information of the currently logged in user. Authentication itself is an interface, and it has many implementation classes:
In this great implementation class, our most commonly used is UsernamePasswordAuthenticationToken
, but when we opened the source code of this class, but found that this class mediocrity, he had only two properties, two constructors as well as a number of get/set
methods; of course, He has more attributes on its parent class.
But from its only two attributes, we can also roughly see that this class saves the basic information of our logged-in user. So how is our login information stored in these two objects? This is going to sort out the login process.
Two, login process
In Spring Security, the verification of authentication and authorization is completed in a series of filter chains. In this series of filter chains, the filters related to authentication are the UsernamePasswordAuthenticationFilter
space issue, I will list them here Several important methods in this class:
publicclass UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
String username = obtainUsername(request);
String password = obtainPassword(request);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
setDetails(request, authRequest);
returnthis.getAuthenticationManager().authenticate(authRequest);
}
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(passwordParameter);
}
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
}
protected void setDetails(HttpServletRequest request,
UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
}
According to this source code we can see:
(1) through the first obtainUsername
and obtainPassword
extraction methods inside a request username / password out, extraction method is request.getParameter, which is why the Spring Security default forms login parameters to be passed through in the form of key / value, and can not pass parameters JSON , If you want to pass JSON parameters, just modify the logic here.
(2) After obtaining the request was passed to the username / password, then on a configuration UsernamePasswordAuthenticationToken
object passed username
and password
, username
correspond to UsernamePasswordAuthenticationToken
the principal
attributes, and password
is corresponding to its credentials
attributes.
(3) Next, setDetails
a method to details
assign attribute, UsernamePasswordAuthenticationToken
itself is not details
property, this property in its parent class AbstractAuthenticationToken
in. details
Is an object that is placed inside the WebAuthenticationDetails
instance of two mainly described information, the request remoteAddress
and the request sessionId
.
(4) The last step is to call the authenticate method to do verification.
Well, from this source code, you can see that all the requested information has basically found its own location, and it will be convenient for us to obtain it in the future.
Next, let's look at the specific verification operation requested.
In the previous attemptAuthentication
method, the final step of the process started checking, verification operation begins to get a AuthenticationManager
, get here is that ProviderManager
, so then we enter into ProviderManager
the authenticate
process, of course, this method is relatively long, I Here are just a few important points:
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
if (result == null && parent != null) {
result = parentResult = parent.authenticate(authentication);
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
((CredentialsContainer) result).eraseCredentials();
}
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
throw lastException;
}
This method is more magical, because almost all the important logic about authentication will be completed here:
(1) First, get authentication
the Class, the current provider to determine whether to support the authentication.
(2) If it supports, call the authenticate method of the provider to start the verification. After the verification is completed, a new Authentication will be returned. I will discuss the specific logic of this method with you in a moment.
(3) provider where there may be more, if the provider is not able to authenticate method returns a normal Authentication
, parent of the provider of the calling authenticate
method to continue checking.
(4) The copyDetails method is used to copy the details property of the old Token to the new Token.
(5) Next, the eraseCredentials method will be called to erase the credential information, which is your password. This erasure method is relatively simple, which is to empty the credentials attribute in the Token.
(6) Finally, publishAuthenticationSuccess
the method will be successful logon event broadcasted.
The general process is the above. In the for loop, the provider you get for the first time is an AnonymousAuthenticationProvider. This provider does not support UsernamePasswordAuthenticationToken at all, that is, it returns false directly in the provider.supports method, ends the for loop, and then It will enter the next if and directly call the parent's authenticate method to verify.
The parent is ProviderManager
, it will once again return to this authenticate
method. Again back authenticate
method, has become a provider DaoAuthenticationProvider
, the provider is supported UsernamePasswordAuthenticationToken
, it will be well into the class authenticate
method to perform, and DaoAuthenticationProvider
inherited from AbstractUserDetailsAuthenticationProvider
and does not override the authenticate method, so we have finally arrived AbstractUserDetailsAuthenticationProvider#authenticate
approach:
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);
postAuthenticationChecks.check(user);
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
The logic here is relatively simple:
(1) First extract the login user name from Authentication.
(2) then took by username
calling to retrieveUser
the method to get the current user object, this step will call our own during login to write loadUserByUsername
method, so there is actually a return to the user objects you login, you can refer to micro personnel org / javaboy /vhr/service/HrService.java#L34.
(3) Next, call the preAuthenticationChecks.check
method to examine each user account properties in the state is normal, for example, whether the account is disabled, whether the account is locked, the account has expired and so on.
(4) The additionalAuthenticationChecks
method is to do a password comparison. Many friends are curious about how Spring Security's passwords are encrypted and how to compare. You can understand it here. Because the logic of the comparison is very simple, I will not post the code here. .
(5) Finally, postAuthenticationChecks.check
check whether the password is expired methods.
(6) Then there is a forcePrincipalAsString
property, whether this is to force Authentication
the principal
property to a string, we start at this property UsernamePasswordAuthenticationFilter
is actually set to the string (ie username) class, but by default, when a user logs After success, the value of this attribute becomes the object of the current user. The reason for this is because forcePrincipalAsString
the default is false, but do not change this fact, use false, so to get the current user information in the latter part of the time but a lot easier.
(7) Finally, by createSuccessAuthentication
constructing a new method UsernamePasswordAuthenticationToken
.
Okay, so the log-in verification process is now basically repeated with everyone. Then there is another question, where do we look for the logged-in user information?
Three, user information preservation
To find the logged-in user information, we have to solve a problem first, that is, we have said so much above, where did all this start to be triggered?
We came to UsernamePasswordAuthenticationFilter
the parent class AbstractAuthenticationProcessingFilter
, this class we often see, because many times when we want in a Spring Security custom code or login authentication login parameters will be changed to JSON, and we all need custom filters inherited from AbstractAuthenticationProcessingFilter
no doubt, UsernamePasswordAuthenticationFilter#attemptAuthentication
it is to the AbstractAuthenticationProcessingFilter
class doFilter
method is triggered:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
Authentication authResult;
try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
unsuccessfulAuthentication(request, response, failed);
return;
}
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authResult);
}
From the above code, we can see that when the attemptAuthentication
method is called, in fact, trigger a UsernamePasswordAuthenticationFilter#attemptAuthentication
method throws an exception when the login, the unsuccessfulAuthentication
method is called, and when the login is successful, the successfulAuthentication
method will be called, then we take a look at successfulAuthentication
methods:
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
successHandler.onAuthenticationSuccess(request, response, authResult);
}
Here there is a very important code, that is SecurityContextHolder.getContext().setAuthentication(authResult);
, user information is stored in the login is successful here, that is to say, in any place, if we want to get the user login information, are available from the SecurityContextHolder.getContext()
get into, you want to modify, can also be in Modify here.
Finally, everyone also saw one successHandler.onAuthenticationSuccess
. This is the callback method that we configure in SecurityConfig. It is triggered here. You can also refer to the configuration in the micro-personnel org/javaboy/vhr/config/SecurityConfig
.