Project-driven learning, practice to test knowledge
preface
R has previously written two articles on authentication and authorization:
📖 [Project practice] Before using the security framework, I would like to have you hand lift a login authentication
📖 [Project practice] takes you through page permissions, button permissions, and data permissions
In these two articles we have tackled authentication and authorization without using a security framework and understood the core principles. As R said earlier, the core principles are mastered, and any security framework is easy to use! This article explains how to use the mainstream Security framework Spring Security to implement authentication and authorization functions.
Of course, this article will not only explain how to use the framework, but also analyze the source code of Spring Security. By the end of the article, you will find that you have mastered how to use the framework, and have a deep understanding of the framework! If you haven’t read the previous two articles, it’s highly recommended to do so, because security frameworks just encapsulate things for us, and the principles behind them don’t change.
All the code in this article is on Github and can be cloned and run!
grasp
The core of login Authentication in the Web system is the credential mechanism. Whether Session or JWT, a credential is returned to the user upon successful login, and the subsequent user access interface needs to carry the credential to indicate his or her identity. The backend will judge the security of the interface to be authenticated. If there is no problem with the certificate, the interface will be allowed to log in. If there is any problem with the certificate, the request will be directly rejected. This safety judgment is processed uniformly in the filter:
Login authentication is to confirm the identity of a user, and Authorization is to confirm whether a user can access a certain resource. Authorization occurs after authentication. Like authentication, this general logic is a unified operation in filters:
LoginFilter determines the login authentication first, and AuthFilter determines the permission authorization after the authentication succeeds. The actual service logic will be executed only after the authentication succeeds.
Spring Security’s support for Web systems is based on a chain of filters:
User requests go through the Servlet’s filter chain, and in the previous two articles we implemented authentication and authorization through two custom filters! Spring Security does the same for a number of features:
In the Servlet filter chain, Spring Security adds a FilterChainProxy filter to it. The proxy filter creates a set of Spring Security custom filter chains and then executes a series of filters. We can take a look at FilterChainProxy source code:
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {... Omit other code// Get a set of Spring Security filters
List<Filter> filters = getFilters(request);
// Put this set of filters into Spring Security's own filter chain and start executing
VirtualFilterChain vfc = newVirtualFilterChain(fwRequest, chain, filters); vfc.doFilter(request, response); . Omit other code}Copy the code
We can see how many filters Spring Security enables by default:
There we only need to focus on two filters: UsernamePasswordAuthenticationFilter responsible for login authentication, FilterSecurityInterceptor mandate permissions.
💡Spring Security’s core logic is all in this set of filters, filters will call a variety of components to complete the function, master these filters and components you master Spring Security! The framework is used to extend these filters and components.
Keep these words in mind, and when you use and understand Spring Security with these words in mind, you will feel as if you are standing on top and looking down on the whole framework at a glance.
Now that we’ve looked at the big picture, we’re ready to start coding.
To use Spring Security, you must first introduce dependencies (other necessary dependencies for Web projects I’ve covered in previous articles, but I won’t go into more detail here) :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Copy the code
After the dependency packages are imported, Spring Security provides a number of features by default to protect the entire application:
📝 requires authenticated users to interact with the application
📝 creates the default login form
📝 Generates a random password named user and prints it on the console
📝CSRF attack defense and Session Fixation attack defense
📝, etc., etc…….
In real development, these default configurations often do not meet our actual needs, so we usually customize some configurations. The configuration method is very simple, just create a new configuration class:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {}Copy the code
Rewrite WebSecurityConfigurerAdapter method in the class can Spring Security for custom configuration.
Login authentication
After the dependency packages and configuration classes are ready, the first function we need to complete is login authentication, after all, the first step for users to use our system is login. The previous article introduced two authentication methods: Session and JWT. Here we use Spring Security to implement these two authentication methods.
The simplest authentication method
No matter which authentication method and framework, some core concepts will not change, these core concepts in the security framework will be reflected in various components, understanding each component at the same time the function will follow the implementation of the function.
There are many users on our system, and confirming which user is currently using our system is the ultimate purpose of login authentication. Here we have extracted a core concept: current logged-in user/current authenticated user. The whole system security is around the current login user! This is easy to understand, if the current login user can not confirm, then A placed an order to B’s account, this is not A mess. This concept is embodied in Spring Security as 💡Authentication, which stores Authentication information on behalf of the current logged-in user.
How do we get it and use it in our program? We need to obtain Authentication via 💡SecurityContext. If you read the previous article, you might have guessed that this SecurityContext is our context object!
Such objects that need to be passed across several method calls in a thread are often referred to as contexts. Context objects are very necessary, otherwise you have to add an extra parameter to receive objects for each method, which is too much trouble.
The context object is managed by 💡SecurityContextHolder and can be used anywhere in the application:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Copy the code
The link looks like this: SecurityContextHolder👉SecurityContext👉Authentication.
The SecurityContextHolder principle is very simple: just like the context object we implemented earlier, we use a ThreadLocal to ensure that the same object is passed from thread to thread! Source code I will not paste, specific can see before the article to write context object implementation.
We now know the three core components of Spring Security:
📝Authentication: stores Authentication information and represents the current login user
📝SeucirtyContext: a context object used to obtain Authentication
📝SecurityContextHolder: A context management object used to retrieve the SecurityContext anywhere in the program
Their relationship is as follows:
The three things in Authentication are the Authentication information:
📝Principal: Indicates the user information. If there is no authentication, it is the user name. After authentication, it is the user object
📝Credentials: user Credentials, usually passwords
📝Authorities: User permissions
Now that we know how to get and use the current logged-in user, how is the user authenticated? I can’t just create a new one just because the user is authenticated. So we’re missing an Authentication process that generates an Authentication object!
The authentication process is the login process, without the use of security framework our authentication process is like this:
Query user data 👉 check whether the account password is correct 👉 If yes, the user information is stored in the 👉 context. If the object exists in the 👉 context, the user logs in
The same goes for Spring Security’s authentication process:
Authentication authentication = newUsernamePasswordAuthenticationToken (user name, the user password, user permissions set); SecurityContextHolder.getContext().setAuthentication(authentication);Copy the code
As with not using a security framework, putting authentication information into context means that the user is logged in. This code demonstrates the simplest way to authenticate Spring Security. Simply place Authentication into the SecurityContext.
This process is the reverse of the previous one: Authentication👉SecurityContext👉SecurityContextHolder.
Do you think that’s it? That completes the certification? That’s too easy. For Spring Security, this does complete authentication, but for us there is one less step, which is to determine if the user’s account password is correct. When the user logs in, the user will pass the account password. We must query the user data and then judge whether the account password passed is correct. Only if it is correct, we will put the authentication information into the context object.
// Call the service layer to perform the judgment business logic
if(! Userservice.login (User name, user password) {return "Incorrect account password";
}
// If the user password is correct, the authentication information will be placed in the context.
Authentication authentication = newUsernamePasswordAuthenticationToken (user name, the user password, user permissions set); SecurityContextHolder.getContext().setAuthentication(authentication);Copy the code
This is a complete authentication process, the same as the process without the security framework, except that some of the components were implemented by ourselves.
Here, we write all the logic in the business layer to query user information and verify the account password by ourselves. In fact, Spring Security also has components for us to use:
AuthenticationManager Authentication mode
💡AuthenticationManager is the component that Spring Security uses to perform authentication. You only need to call its Authenticate method to complete authentication. Spring Security authentication is by default in UsernamePasswordAuthenticationFilter call this component in the filter, the filter is responsible for the authentication logic.
To use this component in our own way, we need to configure the class before:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
protected AuthenticationManager authenticationManager(a) throws Exception {
return super.authenticationManager(); }}Copy the code
Here we write the full login interface code:
@RestController
@RequestMapping("/API")
public class LoginController {
@Autowired
private AuthenticationManager authenticationManager;
@PostMapping("/login")
public String login(@RequestBody LoginParam param) {
// Generate an authentication message containing the account password
Authentication token = new UsernamePasswordAuthenticationToken(param.getUsername(), param.getPassword());
// AuthenticationManager verifies this Authentication information and returns an authenticated Authentication
Authentication authentication = authenticationManager.authenticate(token);
// Store the returned Authentication in the context
SecurityContextHolder.getContext().setAuthentication(authentication);
return "Login successful"; }}Copy the code
Notice that the process is exactly the same as before, except that user authentication is performed using AuthenticationManager instead.
The validation logic of AuthenticationManager is very simple:
Query the user object based on the user name (if the user name is not found, an exception is thrown)👉 Verifies the password of the user object with the passed password. If the password does not match, an exception is thrown
There’s nothing to add to this logic. It’s simple enough. The point is that Spring Security provides components for each of these steps:
📝 Who performs the logic of querying user objects by user name? User object data can be stored in memory, in a file, or in a database. This part is handled by 💡UserDetialsService. This interface has only one method loadUserByUsername(String username), which queries the user object by the username. The default implementation is in memory.
📝 what is the user object that comes out of the query? The user object data in each system is different, and we need to confirm what our user data looks like. User data in Spring Security is represented by 💡UserDetails, which provides common attributes such as account numbers and passwords.
📝 to verify the password we may feel relatively simple, if, else fix, there is no need to use what components? But after all, the framework is a framework to consider more comprehensive, in addition to if, else also solved the problem of password encryption, this component is 💡PasswordEncoder, responsible for password encryption and verification.
We can look at the source of the AuthenticationManager verification logic:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {... Omit other code// The user name passed in
String username = authentication.getName();
// Call the UserDetailService method to query the user object UserDetail based on the user name (if the query fails, the UserDetailService will throw an exception)
UserDetails userDetails = this.getUserDetailsService().loadUserByUsername(username);
String presentedPassword = authentication.getCredentials().toString();
// Pass the password
String password = authentication.getCredentials().toString();
// Use PasswordEncoder to check whether the passed password matches the real user password
if(! passwordEncoder.matches(password, userDetails.getPassword())) {// An exception is thrown if the password is incorrect
throw new BadCredentialsException("Error message...");
}
// The whole UserDetails is put in as the Principal
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userDetails,
authentication.getCredentials(), userDetails.getAuthorities());
returnresult; . Omit other code}Copy the code
UserDetialsService👉UserDetails👉PasswordEncoder, these three components of Spring Security have a default implementation, which generally can not meet our actual requirements, so here we will implement these components!
Encryption device PasswordEncoder
The first is PasswordEncoder. This interface is very simple with two important methods:
public interface PasswordEncoder {
/** * encryption */
String encode(CharSequence rawPassword);
/** * Validates unencrypted strings (passwords passed from the front end) with encrypted strings (passwords stored in the database) */
boolean matches(CharSequence rawPassword, String encodedPassword);
}
Copy the code
You can implement this interface to define your own encryption and validation rules, but Spring Security provides many cryptographic implementations, so we’ll just pick one. You can do the following in the configuration class described earlier:
@Bean
public PasswordEncoder passwordEncoder(a) {
// Here we use bcrypt encryption algorithm, which is relatively high security
return new BCryptPasswordEncoder();
}
Copy the code
Because password encryption is one of the few features I didn’t cover in the previous article, I’ll mention it here. Before adding user data to the database, encrypt the password. Otherwise, the plaintext password obtained from the database cannot pass the verification. For example, we have an interface for user registration:
@Autowired
private PasswordEncoder passwordEncoder;
@PostMapping("/register")
public String register(@RequestBody UserParam param) {
UserEntity user = new UserEntity();
// Call the encryptor to encrypt the password passed from the front end
user.setUsername(param.getUsername()).setPassword(passwordEncoder.encode(param.getPassword()));
// Add the user entity object to the database
userService.save(user);
return "Registration successful";
}
Copy the code
The password stored in the database is encrypted:
The user object UserDetails
This interface is what we call a user object, which provides some common attributes for the user:
public interface UserDetails extends Serializable {
/** * User permissions set (this permission object is now ignored, I will explain when it comes to permissions) */
Collection<? extends GrantedAuthority> getAuthorities();
/** * User password */
String getPassword(a);
/** * User name */
String getUsername(a);
/** * Returns true if the user has not expired, false */ if the user has not expired
boolean isAccountNonExpired(a);
/** * Returns true if the user is not locked or false */ if the user is not locked
boolean isAccountNonLocked(a);
/** * Returns true if the user credential (usually a password) has not expired, false */ otherwise
boolean isCredentialsNonExpired(a);
/** * Returns true if the user is enabled, false if the user is not
boolean isEnabled(a);
}
Copy the code
In practice, we have a variety of user attributes, and these default attributes are not necessarily satisfied, so we usually implement the interface ourselves, and then set up our actual user entity objects. Method to implement this interface to rewrite a lot more troublesome, we can inherit the Spring Security provided by the org. Springframework. Security. Core. Populated userdetails. The User class, This class implements the UserDetails interface and saves us the work of overriding methods:
public class UserDetail extends User {
/** * Our own user entity object, to retrieve the user information directly from this entity object. (I won't write get/set here) */
private UserEntity userEntity;
public UserDetail(UserEntity userEntity, Collection<? extends GrantedAuthority> authorities) {
// The parent constructor must be called to initialize the user name, password, and permission
super(userEntity.getUsername(), userEntity.getPassword(), authorities);
this.userEntity = userEntity; }}Copy the code
Business object UserDetailsService
The interface is simple and has only one method:
public interface UserDetailsService {
/** * Get user object based on user name */
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
Copy the code
Our own user business class this interface can complete its own logic:
@Service
public class UserServiceImpl implements UserService.UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) {
// Query the user entity from the database
UserEntity user = userMapper.selectByUsername(username);
// This exception must be thrown if it is not found, so that it can be handled by Spring Security's error handler
if (user == null) {
throw new UsernameNotFoundException("User not found");
}
// Return our custom UserDetail object.
return newUserDetail(user, Collections.emptyList()); }}Copy the code
AuthenticationManager verifies the three components that are called and we are ready to implement it!
I don’t know if you’ve noticed, but Spring Security’s custom exception is thrown whenever we fail to query the user or verify the password. These abnormalities may not, Spring Security for these exceptions are handled in ExceptionTranslationFilter filter (can review the previous filter screenshots), And 💡AuthenticationEntryPoint specializes in handling authentication exceptions!
Authentication exception handler AuthenticationEntryPoint
The interface also has only one method:
public interface AuthenticationEntryPoint {
/** * receive exception and handle */
void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException);
}
Copy the code
We customize a class to implement our own error handling logic:
public class MyEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
response.setContentType("application/json; charset=utf-8");
PrintWriter out = response.getWriter();
// A front-end authentication error is displayed
out.write("Authentication error"); out.flush(); out.close(); }}Copy the code
Users pass the account password 👉 authentication verification 👉 exception handling, this set of components of the process we have all defined! There is only one final step left to do some configuration in the Spring Security configuration class to make this work.
configuration
What interfaces Spring Security protects, what components are in effect, whether certain features are enabled, and so on need to be configured in the configuration class. Note the code comments:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserServiceImpl userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
// Turn CSRF and frameOptions off. If CSRF is closed, it will affect the front-end request interface.
http.csrf().disable();
http.headers().frameOptions().disable();
// Enable cross-domain so that the front-end calls the interface
http.cors();
// This is the key to configuration, deciding which interfaces are enabled and which are bypassed
http.authorizeRequests()
// Note that this is a necessary configuration to allow front-end cross-domain tuning
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
// Specify that certain interfaces can be accessed without authentication. Login and registration interfaces do not require authentication
.antMatchers("/API/login"."/API/register").permitAll()
// All other interfaces need authentication to access
.anyRequest().authenticated()
// Specify the authentication error handler
.and().exceptionHandling().authenticationEntryPoint(new MyEntryPoint());
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// Specify the UserDetailService and the encryptor
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
@Override
protected AuthenticationManager authenticationManager(a) throws Exception {
return super.authenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder(a) {
return newBCryptPasswordEncoder(); }}Copy the code
The most popular of these is the configure(HttpSecurity HTTP) method, which allows you to do a lot of configuration with HttpSecurity. When we override this method, we turn off the default form login, then we configure which components to enable, specify which interfaces to authenticate, and we’re done!
Suppose we now have a /API/test interface. Call this interface when there is no login to see what happens:
Let’s log in:
Then call the test interface:
As you can see, the test interface cannot be accessed properly without logging in, and an error message will be returned following the logic in EntryPoint.
Summary and supplement
Some people may ask, there are a lot of things to configure using AuthenticationManager. Can’t I just use the simplest way mentioned before? Of course, it is possible to use either way, as long as the function can be completed. In fact, no matter which way our authentication logic code is the same as no less, but one is our own business class all done, a component can be integrated framework. Here is also a summary of the process:
- The user invokes the login operation by passing the account and password to the 👉 login interface
AuthenticationManager
- Query user data based on the user name 👉
UserDetailService
Query theUserDetails
- Compare the passed password with the password in the database 👉
PasswordEncoder
- If the verification succeeds, the authentication information is saved in the context 👉
UserDetails
Deposit toAuthentication
That will beAuthentication
Deposit toSecurityContext
- If the authentication fails, an exception is thrown 👉 by
AuthenticationEntryPoint
To deal with
After authentication, Spring Security stores the SecurityContext containing the authentication information into the session. The Key for HttpSessionSecurityContextRepository SPRING_SECURITY_CONTEXT_KEY. That said, you can get the SecurityContext exactly as follows:
SecurityContext securityContext= (SecurityContext)session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY)
Copy the code
Of course, it is not recommended to do this directly, as it is easier to manage using the SecurityContextHolder. In addition to retrieving the current user, it is also convenient to log out using SecurityContextHolder:
@GetMapping("/logout")
public String logout(a) {
SecurityContextHolder.clearContext();
return "Exit successful";
}
Copy the code
Session authentication so far, let’s talk about JWT authentication.
JWT integration
The introduction and utility classes of JWT have been explained very clearly in previous articles, so I will not explain them here, but directly take you to implement the code.
The first step in JWT authentication is to disable session in the configuration class:
/ / disable the session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
Copy the code
Note that by “disable” you mean that Spring Security does not use sessions, it does not mean that you have disabled sessions for the entire system.
Then we modify the login interface. When the user logs in successfully, we need to generate token and return it to the front end, so that the front end can carry token when accessing other interfaces:
@Autowired
private UserService userService;
@PostMapping("/login")
public UserVO login(@RequestBody @Validated LoginParam user) {
// Invoke the business layer to perform the login operation
return userService.login(user);
}
Copy the code
Business layer approach:
public UserVO login(LoginParam param) {
// Query user entity object based on user name
UserEntity user = userMapper.selectByUsername(param.getUsername());
// If no user is found or the password verification fails, a custom exception is thrown
if (user == null| |! passwordEncoder.matches(param.getPassword(), user.getPassword())) {throw new ApiException("Incorrect account password");
}
// The VO object to be returned to the front-end
UserVO userVO = new UserVO();
userVO.setId(user.getId())
.setUsername(user.getUsername())
// Generate a JWT to store the user name data
.setToken(jwtManager.generate(user.getUsername()));
return userVO;
}
Copy the code
Let’s do the login:
We can see that the interface returns the token on successful login, and we need to put the token in the request header when we access other interfaces later. We need to define an authentication filter to verify the token:
@Component
public class LoginFilter extends OncePerRequestFilter {
@Autowired
private JwtManager jwtManager;
@Autowired
private UserServiceImpl userService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
// Retrieve the token string from the request header and parse it.
Claims claims = jwtManager.parse(request.getHeader("Authorization"));
if(claims ! =null) {
// Extract the previously stored username from 'JWT'
String username = claims.getSubject();
// Query the user object
UserDetails user = userService.loadUserByUsername(username);
// Manually assemble an authentication object
Authentication authentication = new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
// Put the authentication object in contextSecurityContextHolder.getContext().setAuthentication(authentication); } chain.doFilter(request, response); }}Copy the code
The logic of the filter is the same as the logic of the simplest Authentication method introduced earlier. Whenever a request comes in, we verify JWT for Authentication. If Authentication is present in the context object, the subsequent filter will know that the request has been authenticated.
Our custom filter needs to replace the Spring Security default authentication filter, so that our filter can take effect, so we need to do the following configuration:
// Insert our custom authentication filter before the default authentication filter
http.addFilterBefore(loginFilter, UsernamePasswordAuthenticationFilter.class);
Copy the code
We can debug with a breakpoint to see what the filter looks like:
Can see we have a custom filter in the filter chain, because there is no enable form authentication so UsernamePasswordAuthenticationFilter will not take effect.
When you access an interface with a token, you can view the result:
Login authentication to the end of this explanation, next we work hard to achieve permission authorization!
Access authorization
Menu permissions are mainly through front-end rendering, data permissions mainly rely on SQL interception, and Spring Security is not very coupled, so it is not much to expand. Let’s comb through the process of granting interface permissions:
- When a request comes in, we first have to know the rules of the request, that is, what kind of permission is required to access
- Then obtain the permissions of the current logged-in user
- Verify that the current user has permission for the request
- If the user has this permission, the data will be returned normally. If the user does not have this permission, the request will be rejected
After completing login authentication, you already have a feeling that Spring Security has broken down the process functionality into a small component for each small function, and we need to customize those components! Spring Security also provides many components for the above process.
Spring Security authorization occurs in FilterSecurityInterceptor filter:
- The first call is 💡SecurityMetadataSource to get the authentication rules for the current request
- Then through
Authentication
Obtain all permission data of the current logged-in user:💡GrantedAuthority
The authentication object holds this permission data, as we mentioned earlier - Call 💡AccessDecisionManager to verify that the current user has the permission
- If yes, the interface is allowed. If no, an exception is thrown, which is handled by 💡AccessDeniedHandler
We can take a look at the source code in the filter:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {... Omit other code// This is an object wrapped by Spring Security that contains information such as request
FilterInvocation fi = new FilterInvocation(request, response, chain);
/ / call the method that the parent class AbstractSecurityInterceptor here, core logic basic authentication in the parent class
InterceptorStatusToken token = super.beforeInvocation(fi); . Omit other code}Copy the code
The beforeInvocation of the parent class looks like this:
protected InterceptorStatusToken beforeInvocation(Object object) {... Omit other code// Call SecurityMetadataSource to get the authentication rule for the current request. ConfigAttribue is the rule, as I'll explain later
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
// If the current request has no rules, it means that the request is accessible without authorization
if (CollectionUtils.isEmpty(attributes)) {
return null;
}
// Get the current login user
Authentication authenticated = authenticateIfRequired();
// Call AccessDecisionManager to verify that the current user has the permission, and throw an exception if the user does not have the permission
this.accessDecisionManager.decide(authenticated, object, attributes); . Omit other code}Copy the code
It’s a cliche. The core process is the same. We then customize these components to complete our own authentication logic.
Authentication rule source SecurityMetadataSource
We only need to focus on one method for this interface:
public interface SecurityMetadataSource {
/** * Gets the authentication rule for the current request *@paramThe object parameter is the FilterInvocation object encapsulated by Spring Security and contains request information *@returnAuthentication rule object */
Collection<ConfigAttribute> getAttributes(Object object);
}
Copy the code
ConfigAttribute is what we call an authentication rule. This interface has only one method:
public interface ConfigAttribute {
/** * This string is a rule. It can be a role name, a permission name, an expression, and so on. * You can define it any way you want, the AccessDecisionManager will use the string */
String getAttribute(a);
}
Copy the code
In the previous article, the implementation of our authorization is based on the resource ID. The user ID is associated with the role ID, and the role ID is associated with the resource ID. In this way, the user is associated with the resource, and our interface resources are reflected in the database like this:
Here again, we use the resource ID as the permission marker. Let’s customize the SecurityMetadataSource component:
@Component
public class MySecurityMetadataSource implements SecurityMetadataSource {
/** * all interface resource objects in the current system are placed here as a function of a cache. * You can initialize the cache when the application starts, or load data during use, but I won't expand here
private static final Set<Resource> RESOURCES = new HashSet<>();
@Override
public Collection<ConfigAttribute> getAttributes(Object object) {
// This object is wrapped by Spring Security for us. We can use this object to obtain information such as request
FilterInvocation filterInvocation = (FilterInvocation) object;
HttpServletRequest request = filterInvocation.getRequest();
// Traverses all permission resources to match the current request
for (Resource resource : RESOURCES) {
GET:/API/user/test/{id}, the colon before the request method, after the colon request path, so the string split
String[] split = resource.getPath().split(":");
// Because /API/user/test/{id} does not directly determine whether the request path matches, we need to use the Ant class to match
AntPathRequestMatcher ant = new AntPathRequestMatcher(split[1]);
// If both the request method and the request path match, the requested permission resource is found
if (request.getMethod().equals(split[0]) && ant.matches(request)) {
// Return our permission resource ID. This SecurityConfig is a simple implementation of ConfigAttribute
return Collections.singletonList(newSecurityConfig(resource.getId().toString())); }}// The request can be accessed without authorization
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes(a) {
// Don't worry, just write it this way
return null;
}
@Override
public boolean supports(Class
clazz) {
// Don't worry, just write it this way
return true; }}Copy the code
Note that the ConfigAttribute authentication rule we return is our resource ID.
User permission GrantedAuthority
This component represents the permissions that the user has and, like ConfigAttribute, has only one method, which returns a string that represents the permissions
public interface GrantedAuthority extends Serializable {
String getAuthority(a);
}
Copy the code
Compare GrantedAuthority with ConfigAttribute to see if the user has a privilege.
Spring Security has a simple implementation of GrantedAuthority. SimpleGrantedAuthority is good enough for us, so let’s create an additional implementation. UserDetialsService (UserDetialsService, UserDetialsService, UserDetialsService)
@Override
public UserDetails loadUserByUsername(String username) {
UserEntity user = userMapper.selectByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found");
}
// Query all resource ids owned by the user, and then convert them to 'SimpleGrantedAuthority'
Set<SimpleGrantedAuthority> authorities = resourceService.getIdsByUserId(user.getId())
.stream()
.map(String::valueOf)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
// Put both the user entity and the permission set in UserDetail,
return new UserDetail(user, authorities);
}
Copy the code
When Authentication is complete, Authentication will have user information and permission data.
Authorization management AccessDecisionManager
It’s finally time to get to our actual authorization component, which ultimately determines whether or not you have a certain authorization. The interface only needs to focus on one method:
public interface AccessDecisionManager {
/** * Authorizes the operation, if there is no permission to throw an exception **@paramAuthentication Current login user to obtain the permission information of the current user *@paramObject FilterInvocation object to obtain request information *@paramConfigAttributes Current request authentication rule */
void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException;
}
Copy the code
If this method accepts these parameters, it will be able to perform permissions verification.
@Component
public class MyDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) {
// If the authorization rule is empty, this URL can be accessed without authorization
if (Collections.isEmpty(configAttributes)) {
return;
}
// Check whether the authorization rule matches the permission of the current user
for (ConfigAttribute ca : configAttributes) {
for (GrantedAuthority authority : authentication.getAuthorities()) {
// If a match is found, the current user has the permission
if (Objects.equals(authority.getAuthority(), ca.getAttribute())) {
return; }}}// The exception must be thrown, otherwise the error handler will not catch it
throw new AccessDeniedException("No relevant authority.");
}
@Override
public boolean supports(ConfigAttribute attribute) {
// Don't worry, just write it this way
return true;
}
@Override
public boolean supports(Class
clazz) {
// Don't worry, just write it this way
return true; }}Copy the code
Authorization error handler AccessDeniedHandler
This component has only one method to handle exceptions, just like the previous authentication exception handler, except that this one handles authorization exceptions. Let’s directly implement:
public class MyDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("application/json; charset=utf-8");
out.write("No relevant authority."); out.flush(); out.close(); }}Copy the code
configuration
Now that the components are defined, the last step is to make them work. Our authentication rules source component SecurityMetadataSource the AccessDecisionManager component must pass authentication and authorization management filter FilterSecurityInterceptor to configure effect, so we have to write a filter, The core code of this filter is basically written according to the parent class, mainly is the configuration of attributes:
@Component
public class AuthFilter extends AbstractSecurityInterceptor implements Filter {
@Autowired
private SecurityMetadataSource securityMetadataSource;
@Override
public SecurityMetadataSource obtainSecurityMetadataSource(a) {
// return our custom SecurityMetadataSource
return this.securityMetadataSource;
}
@Override
@Autowired
public void setAccessDecisionManager(AccessDecisionManager accessDecisionManager) {
// Inject our custom AccessDecisionManager
super.setAccessDecisionManager(accessDecisionManager);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// The following is the superclass
FilterInvocation fi = new FilterInvocation(request, response, chain);
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
// Execute the next interceptor
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
// Processing after the request
super.afterInvocation(token, null); }}@Override
publicClass<? > getSecureObjectClass() {return FilterInvocation.class;
}
@Override
public void init(FilterConfig filterConfig) {}
@Override
public void destroy(a) {}}Copy the code
With the filter defined, let’s go back to the Spring Security configuration class and insert the filter into the existing authentication filter:
http.addFilterBefore(authFilter, FilterSecurityInterceptor.class);
Copy the code
We can see the effect of accessing the interface without permission:
Access interfaces with permissions:
conclusion
That’s the end of Spring Security, and we’ve achieved our functionality with a custom implementation of two filters and N + components. Here’s a mind map for you to understand:
Although there are so many components, the core process and some concepts of authentication and authorization will not change, and any security framework will change. Shiro, for example, has a basic concept where Subject represents the current user and SubjectManager is the user manager…
In my previous two articles, someone also talked about how writing by hand is better than using a security framework. Indeed, writing by hand can be as flexible as possible (and not complicated), while using a security framework can be tied to the framework’s stereotype. So what are the advantages of secure frames versus handwriting? I think the advantages are as follows:
- Some features, such as Spring Security’s encryptors, come in handy right out of the box
- The framework stereotype is both a constraint and a norm, and whoever takes on your project sees a familiar security framework and starts working on it immediately
All code and SQL statements in this article are stored on Github and can be cloned and run.
Reprint please contact the public number [RudeCrab] open the white list