Spring Security is introduced
-
Spring Security is a permission management framework based on the Spring framework
-
The predecessor of Spring Security is Acegi Security
Acegi Security is criticized for its cumbersome configuration. After it was put into Spring embrace, with the rise of SpringBoot, the ease of use of Spring Security has been greatly improved, and it is often used in SpringBoot and SpringCloud projects
-
Basic features of Spring Security
- Authentication: Provides various common authentication methods
- Authorization: Provides URL-based request authorization, support method access authorization, and object access authorization
The basic principle of
-
Spring Security processes Web requests through layers of filters
In the Filter chain, the authentication and authorization are completed step by step. If any exception is found, the exception handler will handle it
-
Core concepts in filter chains
-
springSecurityFilterChain
Spring Security at the core of the filter is called springSecurityFilterChain, type is a named FilterChainProxy
-
WebSecurity, HttpSecurity
WebSecurity builds the FilterChainProxy object
HttpSecurity builds a SecurityFilterChain in FilterChainProxy
-
WebSecurityConfiguration
The @enableWebSecurity annotation imports the WebSecurityConfiguration class
WebSecurityConfiguration creates the builder object WebSecurity and the core filter FilterChainProxy
-
-
Common components of Spring Security
- Authentication: an Authentication interface that defines the data form of an Authentication object.
- AuthenticationManager: Used to verify Authentication and returns a value after Authentication
- SecurityContext: The context object used to store Authentication
- SecurityContextHolder: Used to access the SecurityContext
- GrantedAuthority: indicates the permission
- UserDetails: indicates the user information
- UserDetailsService: Obtains user information
Simple to use
-
Introduce Spring Security dependencies
<! Spring Security--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> Copy the code
After importing the dependency, Spring Security takes effect automatically without any configuration and the request is redirected to the login page
Default user names, passwords, and permissions can be configured in application.yaml
spring: security: user: name: ming password: 123456 roles: admin Copy the code
-
Memory-based authentication
@Configuration @EnableWebSecurity // Enable annotation Settings @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { // Configure the password encryptor @Bean public PasswordEncoder passwordEncoder(a) { return new BCryptPasswordEncoder(); } // Configure the authentication manager @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("admin") .password(passwordEncoder().encode("123")).roles("admin") .and() .withUser("user") .password(passwordEncoder().encode("456")).roles("user"); } // Configure security policies @Override protected void configure(HttpSecurity http) throws Exception { // Set the path and required permissions, support ant style path writing method http.authorizeRequests() // Set OPTIONS to try to pass the request directly .antMatchers(HttpMethod.OPTIONS, "/ * *").permitAll() .antMatchers("/api/demo/user").hasAnyRole("user"."admin") // Note that the hasAnyAuthority role must start with ROLE_ .antMatchers("/api/demo/admin").hasAnyAuthority("ROLE_admin") .antMatchers("/api/demo/hello").permitAll() .and() // Enable form login .formLogin().permitAll() .and() // Enable logout.logout().permitAll(); }}Copy the code
Front end separation
Disable CSRF defense and session management
CSRF defense requires that the CSRF Token must be carried in the form login and do not need to be enabled when the front and back ends are separated
Session management is set to STATELESS and STATELESS JWT is used for authentication
@Override
protected void configure(HttpSecurity http) throws Exception {
// Disable CSRF defense
http.csrf().disable();
// Disable session management
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// ...
}
Copy the code
Custom login logic
Spring Security default login form, to support JSON request, inheritable UsernamePasswordAnthenticationFilter, and use HttpSecurity addFilterAt replacing the original
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// Determine whether the request is in JSON format
if(request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){
// ...
} else {
return super.attemptAuthentication(request, response); }}}Copy the code
By configuring AuthenticationManagerBuilder, set the custom UserDetailsService
@Autowired
private CustomUserDetailsService customUserDetailsService
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService)
.passwordEncoder(passwordEncoder());
}
Copy the code
Implement the loadUserByUsername method of UserDetailsService
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// Query the user based on username
User user = userMapper.getUserByUsername(s);
if (user == null) {
// ...
}
// Query roles or permissions
List<SimpleGrantedAuthority> authorities = userMapper.listRolesByUsername(s)
.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// Construct the UserDetails instance and return}}Copy the code
Custom login success handler
Set up a custom successHandler by configuring HttpSecurity
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().permitAll()
.loginProcessingUrl("/login")
.successHandler(customLoginSuccessHandler)
}
Copy the code
CustomLoginSuccessHandler, returned to the front in the form of JSON, carrying the generated Token
@Component
@RequiredArgsConstructor
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler {
private final JwtUtil jwtUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
// Construct a uniform return format object
Map<String, Object> res = new HashMap<>();
res.put("code".200);
res.put("message": "Certification successful");
res.put("path": "login");
Object principal = authentication.getPrincipal();
if (principal instanceof User) {
// Based on user information, use JWT utility classes to build tokens
// ...
// Save to the returned contents
res.put("data"."xxxxxx")}// Write response in JSON format
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8"); PrintWriter writer = response.getWriter(); writer.print(JsonUtil.Obj2Str(res)); writer.flush(); }}Copy the code
Custom logon failure handler
This section describes how to configure HttpSecurity to set a customized failureHandler
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().permitAll()
.loginProcessingUrl("/login")
.failureHandler(customLoginFailureHandler)
}
Copy the code
CustomLoginFailureHandler, return authentication failure and the failure information
@Component
public class CustomLoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) {
// Encapsulate the unified return format object
Res<Object> res = Res.of(ResCode.TOKEN_CREATE_FAIL).path("/login");
// Set failure information according to the exception
if (exception instanceof LockedException) {
res.errorMsg("Account locked");
} else if (exception instanceof CredentialsExpiredException) {
res.errorMsg("Password expired");
} else if (exception instanceof AccountExpiredException) {
res.errorMsg("Account expired");
} else if (exception instanceof DisabledException) {
res.errorMsg("Account disabled");
} else if (exception instanceof BadCredentialsException) {
res.errorMsg("Incorrect username or password");
}
// The enclosed JSON format writes the Response tool methodWebUtil.writeJsonToResponse(response, JsonUtil.objToStr(res)); }}Copy the code
Custom unlogged processor
Configuration authenticationEntryPoint
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(customAuthenticationEntryPoint)
}
Copy the code
CustomAuthenticationEntryPoint
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
// Construct unlogged returned contentRes<Object> res = Res.of(ResCode.TOKEN_NOT_EXIST) .path(request.getRequestURI()); WebUtil.writeJsonToResponse(response, JsonUtil.objToStr(res)); }}Copy the code
Insufficient custom permission processor
Configuration accessDeniedHandler
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.accessDeniedHandler(customAccessDeniedHandler);
}
Copy the code
CustomAccessDeniedHandler
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
// Construct the content returned with insufficient permissionsRes<Object> res = Res.of(ResCode.TOKEN_NO_AUTHORITY) .path(request.getRequestURI()); WebUtil.writeJsonToResponse(response, JsonUtil.objToStr(res)); }}Copy the code
Customize logout success logic
Configuration logoutSuccessHandler
@Override
protected void configure(HttpSecurity http) throws Exception {
http.logout().permitAll()
.logoutUrl("/logout")
.logoutSuccessHandler(logoutSuccessHandler);
}
Copy the code
CustomLogoutSuccessHandler
@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// Construct the contents returned after a successful logout
Res<String> res = Res.ok("Logout successful").path("/logout"); WebUtil.writeJsonToResponse(response, JsonUtil.objToStr(res)); }}Copy the code
You can also configure the logout handling logic using addLogoutHandler for HttpSecurity
Custom JWT filters
Add JWT filters to the filter chain
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(jwtAuthenticationTokenFilter,
UsernamePasswordAuthenticationFilter.class);
}
Copy the code
JwtAuthenticationTokenFilter
@Component
@RequiredArgsConstructor
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private final UserDetailsService userDetailsService;
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// Fetch the token in the header for verification
String authHeader = httpServletRequest.getHeader(jwtUtil.getHeader());
if(authHeader ! =null && !StringUtil.isEmpty(authHeader)) {
String username = jwtUtil.getUsernameFromToken(authHeader);
if(username ! =null
&& SecurityContextHolder.getContext().getAuthentication() == null) {
// Query the user according to username
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
/ / check
if (jwtUtil.validateToken(authHeader, userDetails)) {
/ / build the authentication
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails,
null,
userDetails.getAuthorities());
// Set details, including address, session, etc
authentication.setDetails(new
WebAuthenticationDetails(httpServletRequest));
// Set authentication to the context objectSecurityContextHolder.getContext().setAuthentication(authentication); } } } filterChain.doFilter(httpServletRequest, httpServletResponse); }}Copy the code
Dynamically configure URL permissions
Spring Security in the filter chain contains many filters, including FilterSecurityInterceptor is very important, completed the main authentication logic
BeforeInvocation method
attemptAuthorization
It can be seen from the source code that there are two ways to dynamically configure URL permissions
-
Customize SecurityMetadataSource to load ConfigAttribute from the data source
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource { private final AntPathMatcher antPathMatcher = new AntPathMatcher(); private final FilterInvocationSecurityMetadataSource superMetadataSource; private final Map<String, String[]> urlRoleMap = new HashMap<>(); public MySecurityMetadataSource( FilterInvocationSecurityMetadataSource metadataSource) { this.superMetadataSource = metadataSource; // The permission configuration can be loaded from the database urlRoleMap.put("/api/demo/admin".new String[]{"ROLE_admin"}); urlRoleMap.put("/api/demo/user".new String[]{"ROLE_user"."ROLE_admin"}); } @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { FilterInvocation fi = (FilterInvocation) object; String url = fi.getRequestUrl(); for (Map.Entry<String, String[]> entry : urlRoleMap.entrySet()) { if (antPathMatcher.match(entry.getKey(), url)) { / / generated ConfigAttribute returnSecurityConfig.createList(entry.getValue()); }}// Returns the default permission configuration defined by the configuration class returnsuperMetadataSource.getAttributes(object); }}Copy the code
Due to SecurityConfig. CreateList returns SecurityConfig ConfigAttribute type, Default WebExpressionVoter vote is used to verify the WebExpressionConfigAttribute type, therefore also need to configure a RoleVoter
WebExpressionConfigAttribute refers to the configuration by HttpSecurity allocation in the class of permissions
Configuration HttpSecurity
http.authorizeRequests() .anyRequest().authenticated() .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { // Set it to a custom SecurityMetadataSource object.setSecurityMetadataSource(mySecurityMetadataSource); AffirmativeBased is a type of AccessDecisionManager AffirmativeBased, there is a voting machine to pass Thursday, Thursday, and Thursday, respectively, a member of the voting body. // Thursday, however, a member of the voting body does not pass, and both abstain object.setAccessDecisionManager(new AffirmativeBased( Arrays.asList( new WebExpressionVoter(), new RoleVoter() ))); returnobject; }})Holding a ConfigAttribute from a database dynamically, however, makes it possible to vote for each ConfigAttribute in a multiple-member machine. */ is passed only when all permissions are granted Copy the code
-
Customize a voter, in the voter can obtain URL, dynamic load permissions, see RoleVoter
public class CustomRoleVoter extends RoleVoter { @Override public int vote(Authentication authentication, Object object, Collection
attributes) { if (authentication == null) { return ACCESS_DENIED; } List<ConfigAttribute> dbAttributes = new ArrayList<>(); FilterInvocation fi = (FilterInvocation) object; String url = fi.getRequestUrl(); // Obtain permissions from the data source according to the URL and save to dbAttributes // ... int result = ACCESS_ABSTAIN; // Obtain the authentication permission Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); // Check whether authentication contains permissions for (ConfigAttribute attribute : dbAttributes) { if (attribute.getAttribute() == null) { continue; } if (this.supports(attribute)) { result = ACCESS_DENIED; for (GrantedAuthority authority : authorities) { if (attribute.getAttribute().equals(authority.getAuthority())) { returnACCESS_GRANTED; }}}}returnresult; }}Copy the codeConfiguration HttpSecurity
http.authorizeRequests() .anyRequest().authenticated() .accessDecisionManager(new UnanimousBased( Arrays.asList( new WebExpressionVoter(), new CustomRoleVoter() ))); Thursday, however, differs from Thursday, when both the privileges of a configuration class and a data source are satisfied Copy the code