Spring Security series # 5: User authorization for backend decoupage projects
chapter
Spring Security is a series of simple introduction and practical
The second part of the Spring Security series analyzes the authentication process
Spring Security series 3: Custom SMS Login Authentication
The fourth in the Spring Security series uses JWT for authentication
Spring Security series # 5: User authorization for backend decoupage projects
Spring Security Series 6 authorization Process Analysis
You’re already familiar with login authentication from the previous articles, so let’s start practicing another core feature of Spring Security: user permission authentication. In a system, different users have different permissions. For example, some users can only read a file, while others can modify it. Generally speaking, the system assigns different roles to different users, and each role has a series of permissions.
For a Web system, it is equivalent to specifying which roles users have to access the interface. For now, let’s assume that our system has two roles, one for the general user and one for the administrator. Ordinary users can do things the administrator can do, the administrator can do things ordinary people may not be able to do. Of course, the real business situation may be several times more complex than this, I just throw out a brick to lead jade here, I hope we don’t be ungrateful, continue to ask.
Create two Controller interfaces, one accessible to common users and administrators:
@RestController
@RequestMapping("normal")
public class NormalResourceController {
@GetMapping("/resource")
public ResponseEntity<String> getResource(a){
return ResponseEntity.ok("Normal resource obtained successfully"); }}Copy the code
Create a Controller interface that only administrators can access:
@RestController
@RequestMapping("/admin")
public class AdminResourceController {
@GetMapping("/resource")
public ResponseEntity<String> getResource(a){
return ResponseEntity.ok("Admin resource obtained successfully"); }}Copy the code
Add two roles to the database role table:
id | role_name |
---|---|
1 | admin |
2 | normal_user |
Currently we have two users in the database:
id | username | password |
---|---|---|
1 | test1 | 10 $pjHyw9MSGC i6k546Ii / 0 ulfgtk4wyb4. 8 bsrq7yb4dy. ZpBLxOha |
2 | test2 | 10 $pjHyw9MSGC i6k546Ii / 0 ulfgtk4wyb4. 8 bsrq7yb4dy. ZpBLxOha |
Add admin and normal_user to user test1, normal_user to user test2, add data to user_role table:
id | user_id | role_id |
---|---|---|
1 | 1 | 1 |
2 | 1 | 2 |
3 | 2 | 2 |
Our system’s User object implements the UserDetails interface, and overrides the Getathorities method, which looks up the user’s role information as a GrantedAuthority object:
public class User implements UserDetails {
private static final long serialVersionUID = -16523804109585173L;
private Integer id;
private String username;
private String password;
private List<Role> roleList;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roleList.stream().map(role ->
newSimpleGrantedAuthority(role.getRoleName())).collect(Collectors.toList()); }}Copy the code
Simple implementation
With the above preparations in place, let’s add the following configuration to Security:
@Override
protected void configure(HttpSecurity http) throws Exception {
//super.configure(http);
http.formLogin()
.disable()
// Add header Settings to support cross-domain and Ajax requests for local tests annotated first
//.cors().and()
//.addFilterAfter(corsFilter(), CorsFilter.class)
.apply(smsAuthenticationSecurityConfig).and()
.apply(jwtAuthenticationSecurityConfig).and()
.apply(jwtRequestSecurityConfig).and()
// Set URL authorization
.authorizeRequests()
// The login page must be allowed
.antMatchers("/login"."/verifyCode"."/smsLogin"."/failure"."/jwtLogin").permitAll()
.antMatchers("/admin/**").hasAuthority("admin")
.antMatchers("/normal/**").hasAnyAuthority("admin"."normal_user")
// anyRequest() All requests authenticated() must be authenticated
.anyRequest()
.authenticated().and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
/ / close CSRF
.csrf().disable();
}
Copy the code
Calling antMatchers().hasauthority () indicates that the user must have a role to access these paths.
To test this, we have no problem accessing either controller using the token generated by the test1 user login:
Error 403 is displayed when you use the token generated by user test2 to access admin:
Such a simple user authority authentication is realized.
Custom permission authentication
This is obviously not flexible enough. If we add other interfaces, we still need to configure them here. This kind of hard coding is not very good.
Implement permission authentication interface
To implement dynamic permission authentication, of course, resources must be obtained first, and then the relationship between them and which roles can access them must be represented.
Spring Security is the specific permissions required to access resources through the SecurityMetadataSource, so the first step is to implement SecurityMetadataSource. SecurityMetadataSource is an interface:
public interface SecurityMetadataSource extends AopInfrastructureBean {
Collection<ConfigAttribute> getAttributes(Object var1) throws IllegalArgumentException;
Collection<ConfigAttribute> getAllConfigAttributes(a);
boolean supports(Class
var1);
}
Copy the code
It inherits the AopInfrastructureBean interface, which does not have any methods, but serves as a tag for the base class that implements AOP. If any class implements this interface, then that class will not be proxied by AOP, even if it can be cut in.
Then there are the methods for SecurityMetadataSource:
Collection<ConfigAttribute> getAttributes(Object var1) throws IllegalArgumentException;
Copy the code
Gets the required permission information for a protected security object, returns a set of ConfigAttribute objects, If the security object object is not supported by the current SecurityMetadataSource object, an IllegalArgumentException is thrown.
An object is passed in and the permissions required to access the object are returned. If the current SecurityMetadataSource object does not support the current object, an error is reported, depending on method 3.
Collection<ConfigAttribute> getAllConfigAttributes(a);
Copy the code
Gets the collection of permission information for all security objects held in this SecurityMetadataSource object. The main purpose of this method are AbstractSecurityInterceptor to check each ConfigAttribute object when it is started.
This method does nothing but return a list of all permissions.
Take a look at the relational inheritance diagram for the SecurityMetadataSource interface:
You can see that the SecurityMetadataSource has two subinterfaces,
-
FilterInvocationSecurityMetadataSource is a marker interface, said security object is web request FilterInvocation security metadata sources, in and of itself without any content.
-
MethodSecurityMetadataSource said security object method calls the MethodInvocation metadata sources, the safety of the interface is as follows:
public interface MethodSecurityMetadataSource extends SecurityMetadataSource { Collection<ConfigAttribute> getAttributes(Method method, Class targetClass); } Copy the code
Generally used for permission validation during intermethod calls.
We are here in a web project, implement FilterInvocationSecurityMetadataSource line:
@Component
public class UrlMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
private PowerService powerService;
private final AntPathMatcher matcher = new AntPathMatcher();
public static final String NEED_LOGIN = "NEED_LOGIN";
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) object).getRequestUrl();
List<Power> powers = powerService.queryAll();
for (Power power : powers) {
if (matcher.match(power.getUrl(),requestUrl)){
// There is a route in the database, which requires the role to access it
List<Role> roleList = power.getRoleList();
if (CollectionUtils.isEmpty(roleList)){
break;
}
return roleList.stream().map(item -> newSecurityConfig(item.getRoleName().trim())).collect(Collectors.toList()); }}// The path has no role to access
return SecurityConfig.createList(NEED_LOGIN);
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes(a) {
return null;
}
/** * tells the caller whether SecurityMetadataSource currently supports such security objects, and only if it does, can the getAttributes method */ be called on such security objects
@Override
public boolean supports(Class
clazz) {
returnclazz.isAssignableFrom(FilterInvocation.class); }}Copy the code
First of all, we obtained the currently accessed resources from request, and then used PowerService to query all resources in the database. The resource class is as follows:
@Data
public class Power implements Serializable {
private static final long serialVersionUID = -25876673587503659L;
private Integer id;
private String title;
private String url;
private List<Role> roleList;
}
Copy the code
Then compare the request URL with all url patterns queried in the database one by one to see which URL pattern matches, and then obtain the role corresponding to the URL pattern.
If the getAttributes(Object O) method returns NULL, that means the current request does not require any role access, or even a login. However, in my entire business, there is no such request, and all unmatched paths need to be authenticated before they can be accessed, so I return a NEED_LOGIN role that does not exist in the database, so I will handle this role specifically in the next step of the role comparison process.
The list of roles returned by the getAttributes(Object O) method is ultimately passed to the AccessDecisionManager, so let’s look at the implementation of the AccessDecisionManager.
Implement permission decision maker
Knowing the specific permissions required for the currently accessed URL, it’s now up to you to decide whether the current access can pass permission authentication. The implementation is as follows:
@Component
public class UrlAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) {
for (ConfigAttribute attribute : configAttributes) {
String needRole = attribute.getAttribute();
if (UrlMetadataSource.NEED_LOGIN.equals(needRole) && authentication instanceof AnonymousAuthenticationToken) {
throw new InsufficientAuthenticationException("User needs to log in");
}
// The role of the current user
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
// Compare the roles required for access, as long as one of them is satisfied
for (GrantedAuthority userRole : authorities) {
if (userRole.getAuthority().equals(needRole)){
return; }}}throw new AccessDeniedException("Insufficient authority");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class
clazz) {
return true; }}Copy the code
We’ll focus on the Decide method, which takes three parameters
- Authentication: contains the current user information, including the permissions. The source of permissions here is the user class of our system
getAuthorities
Returns a list of role permissions. - Object:
FilterInvocation
Object, you can getrequest
Such as web resources configAttributes
: It’s on the topgetAttributes
Method to return a list of roles
Because in our system for all the resources, the need to log in to access, through authentication instanceof AnonymousAuthenticationToken judgment have user login, no log in, throw an exception.
Then there is the judgment of user permissions. The judgment condition here is that as long as the current user has a role, he can access the resource. Brothers can also do it according to their own business.
Configuration implementation class
Now that we’ve implemented the resources and validation for the above permissions, it’s time to specify that Spring Security uses our custom implementation class:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SmsAuthenticationSecurityConfig smsAuthenticationSecurityConfig;
@Autowired
private JwtAuthenticationSecurityConfig jwtAuthenticationSecurityConfig;
@Autowired
private JwtRequestSecurityConfig jwtRequestSecurityConfig;
@Autowired
private UrlMetadataSource urlMetadataSource;
@Autowired
private UrlAccessDecisionManager urlAccessDecisionManager;
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/login"."/verifyCode"."/smsLogin"."/failure"."/jwtLogin");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//super.configure(http);
http.formLogin()
.disable()
// Add header Settings to support cross-domain and Ajax requests
//.cors().and()
//.addFilterAfter(corsFilter(), CorsFilter.class)
.apply(smsAuthenticationSecurityConfig).and()
.apply(jwtAuthenticationSecurityConfig).and()
.apply(jwtRequestSecurityConfig).and()
// Set URL authorization
.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(urlAccessDecisionManager);
object.setSecurityMetadataSource(urlMetadataSource);
returnobject; }})// The login page must be allowed
//.antMatchers("/login","/verifyCode","/smsLogin","/failure","/jwtLogin").permitAll()
//.antMatchers("/admin/**").hasAuthority("admin")
//.antMatchers("/normal/**").hasAnyAuthority("admin","normal_user")
// anyRequest() All requests authenticated() must be authenticated
.anyRequest()
.authenticated().and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
/ / close CSRF.csrf().disable(); }}Copy the code
We withObjectPostProcessor method is used here, when creating the default FilterSecurityInterceptor our the accessDecisionManager and securityMetadataSource set in.
It is important to note that we use the original.antmatchers ().permitall () whitelist to block resources. Security actually blocks resources by setting up an anonymous user, which is blocked by our custom UrlMetadataSource. So the whitelist of this place needs to mention the outermost configuration.