Spring Security is introduced

  1. Spring Security is a permission management framework based on the Spring framework

  2. 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

  3. 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

  1. 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

  2. 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

  3. 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

  1. 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
  2. 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

  1. 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
  2. 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 code

    Configuration 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