Spring Boot + Spring Security Basic Getting Started Tutorial

Introduction to Spring Security

Spring Security is a powerful and highly customizable authentication and access control framework. Spring Security focuses on providing authentication and authorization capabilities for Java applications.

Spring Security has two important core functions: user authentication (Authentication) and user authorization (Authorization) .

User authentication: Verify whether a user is a legitimate subject in the system, that is, whether the user can access the system. User authentication generally requires the user to provide a user name and password. The system completes the authentication process by verifying the user name and password.

User authorization: Verify that a user has permission to perform an operation. In a system, different users have different permissions. For example, for a file, some users can only read it, while some users can both read it and modify it. Generally speaking, the system assigns different roles to different users, and each role corresponds to a series of permissions.

Preparation

Create a Spring Boot project

pom.xml file (introduced according to your needs)

    <dependencies>
        <!-- security(安全认证) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- mybatis-plus(数据库操作) -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
        <!-- redis(缓存) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- swagger(api接口文档) -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

        <!-- jjwt(token生成与校验) -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <!-- fastjson2(JSON处理) -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.23</version>
        </dependency>

        <!-- mysql(连接驱动) -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- druid(mysql连接池) -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.16</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

Authentication _

Login verification process

Preliminary study of the principle

The principle of Spring Security is actually a filter chain, which contains filters that provide various functions. Here we can look at the filter in the starter example.

Only core filters are shown in the figure, and other non-core filters are not shown in the figure.

UsernamePasswordAuthenticationFilter : Responsible for processing the login request after we fill in the username and password on the login page. It is mainly responsible for the certification work of the entry case.

ExceptionTranslationFilter: Handles any AccessDeniedException and AuthenticationException thrown in the filter chain.

FilterSecurityInterceptor: A filter responsible for permission verification.

We can view which filters and their order are in the Spring Security filter chain in the current system through Debug.

Click Evaluate Expression or Alt+F8 on the console, as shown below:

 Then enter run.getBean(DefaultSecurityFilterChain.class) to filter, and you can see 15 filters in the run container:

 

Spring Security configuration class

import com.zm.springsecurity.common.filter.CustomAuthenticationFilter;
import com.zm.springsecurity.common.security.CustomAuthenticationFailureHandler;
import com.zm.springsecurity.common.security.CustomAuthenticationSuccessHandler;
import com.zm.springsecurity.common.security.CustomLogoutSuccessHandler;
import com.zm.springsecurity.service.impl.CustomUserDetailsServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级安全
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private static final String URL_WHITELIST[] ={
            "/v2/api-docs", "/swagger-resources/configuration/ui",
            "/swagger-resources", "/swagger-resources/configuration/security",
            "/swagger-ui.html", "/webjars/**", // swagger不需要授权即可访问的路径

            "/login",
            "/logout",
            "/my/login",
            "/my/logout",
            "/captcha",
            "/password",
            "/image/**",
            "/test/**"
    };

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    protected CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
        CustomAuthenticationFilter authenticationFilter = new CustomAuthenticationFilter();
        authenticationFilter.setFilterProcessesUrl("/my/login");
        authenticationFilter.setUsernameParameter("username");
        authenticationFilter.setPasswordParameter("password");
        authenticationFilter.setAuthenticationManager(super.authenticationManager());
        authenticationFilter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler());
        authenticationFilter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler());
        return authenticationFilter;
    }

//    @Override
//    @Bean
//    public AuthenticationManager authenticationManagerBean() throws Exception {
//        return super.authenticationManagerBean();
//    }
//
//    @Override
//    protected AuthenticationManager authenticationManager() throws Exception {
//        return super.authenticationManager();
//    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable() // 开启跨域请求和关闭csrf攻击
                .userDetailsService(new CustomUserDetailsServiceImpl())
//                .formLogin().loginPage("/login_page")
//                .loginProcessingUrl("/my/login")
//                .usernameParameter("username").passwordParameter("password").permitAll()
//                .successHandler(new CustomAuthenticationSuccessHandler()) // 认证成功处理器
//                .failureHandler(new CustomAuthenticationFailureHandler()) // 认证失败处理器
//                .and()
                .logout()
                .logoutUrl("/my/logout")
                .logoutSuccessHandler(new CustomLogoutSuccessHandler()) // 退出登录成功处理器
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // session禁用配置(无状态)
                .and()
                .authorizeRequests()  // 验证请求拦截规则
                .antMatchers(URL_WHITELIST).permitAll() // 配置访问认证白名单
                .antMatchers("/admin/**").hasRole("admin") // 要具有某种权限
                .antMatchers("/user/**").hasAnyRole("admin", "user") // 要具有某种权限中的一种
                .anyRequest().authenticated();

        http.addFilterAt(this.customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

Authenticating with a database

Note: This article uses MyBatis-Plus as the persistence layer framework, and the content related to MyBatis-Plus is written by myself.

Implement the UserDetails interface

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

public class CustomUserDetails implements UserDetails {
    private User user;
    private List<SimpleGrantedAuthority> authorityList;

    public CustomUserDetails() {
    }

    public CustomUserDetails(User user, List<String> roleList) {
        this.user = user;
        this.authorityList = roleList.stream()
                .map(role -> new SimpleGrantedAuthority(role))
                .collect(Collectors.toList());
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorityList;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getPassword();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return user.getStatus();
    }
}

Custom UsernamePasswordAuthenticationFilter filter

If the processing request is form-type data, this step ignores and deletes the content related to CustomAuthenticationFilter in the Security configuration class. UsernamePasswordAuthenticationFilter is an authentication filter. By default, it can only process the data submitted by the form. If you need to process JSON data, you need to override the attemptAuthentication() method of UsernamePasswordAuthenticationFilter.

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;

/**
 * 登录认证过滤器,处理认证的请求体为 JSON 的数据
 */
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String contentType = request.getContentType();
        logger.info("contentType = " + contentType);
        if (contentType.equals(MediaType.APPLICATION_JSON_VALUE) || contentType.equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
            UsernamePasswordAuthenticationToken authRequest = null;
            try (InputStream inputStream = request.getInputStream()) {
                ObjectMapper mapper = new ObjectMapper(); // JSON数据映射器
                Map<String,String> params = mapper.readValue(inputStream, Map.class);
                authRequest = new UsernamePasswordAuthenticationToken(params.get("username"), params.get("password"));
            } catch (IOException e) {
                e.printStackTrace();
                authRequest = new UsernamePasswordAuthenticationToken("", "");
            } finally {
                setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            }
        }
        else {
            return super.attemptAuthentication(request, response);
        }
    }
}

custom processor

  JWT tool class

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.util.StringUtils;

import java.util.Date;

public class JWTUtils {
    private static final String tokenSignKey = "zm_sign_key"; // 私钥(盐),太短会报异常:secret key byte array cannot be null or empty.
    private static final Integer tokenExpiration = 60 * 60 * 24 * 14; // 14天

    public static String createToken(String username){
        String token = Jwts.builder()
                .setSubject("AUTH-USER")
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
                .claim("username", username)
                .signWith(SignatureAlgorithm.HS512, tokenSignKey)
                .compact();
        return token;
    }

    public static String createToken(Long userId, String username){
        String token = Jwts.builder()
                .setSubject("AUTH-USER")
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
                .claim("userId", userId)
                .claim("username", username)
                .signWith(SignatureAlgorithm.HS512, tokenSignKey)
                .compact();
        return token;
    }

    public static Long getUserId(String token) {
        try {
            if (!StringUtils.hasLength(token)) {
                return null;
            }

            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
            Claims claims = claimsJws.getBody();
            return claims.get("userId", Long.class);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public static String getUsername(String token) {
        try {
            if (!StringUtils.hasLength(token)) {
                return "";
            }

            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
            Claims claims = claimsJws.getBody();
            return claims.get("username", String.class);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

Respond to JSON data information

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class ResponseUtils {
    public static void response(HttpServletResponse response, String data) throws IOException {
        response.setContentType("text/html;charset=utf-8");
        PrintWriter responseWriter = response.getWriter();
        responseWriter.write(data);
        responseWriter.flush();
        responseWriter.close();
    }
}

Custom authentication success handler

import com.alibaba.fastjson2.JSON;
import com.zm.springsecurity.utils.JWTUtils;
import com.zm.springsecurity.utils.ResponseUtils;
import com.zm.springsecurity.utils.ResultUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        String username = authentication.getPrincipal().toString();
        String token = JWTUtils.createToken(username);
        String jsonString = JSON.toJSONString(ResultUtils.ok("登录成功", token));
        ResponseUtils.response(response, jsonString);
    }
}

Custom authentication failure handler

import com.alibaba.fastjson2.JSON;
import com.zm.springsecurity.utils.ResponseUtils;
import com.zm.springsecurity.utils.ResultUtils;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        String message = exception.getMessage();
        if(exception instanceof BadCredentialsException){
            message = "用户名或密码错误!";
        }
        String jsonString = JSON.toJSONString(ResultUtils.fail(message));
        ResponseUtils.response(response, jsonString);
    }
}

Custom logout success handler

import com.alibaba.fastjson2.JSON;
import com.zm.springsecurity.utils.ResponseUtils;
import com.zm.springsecurity.utils.ResultUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        String jsonString = JSON.toJSONString(ResultUtils.ok("退出登录成功!"));
        ResponseUtils.response(response, jsonString);
    }
}

Authorization _ _

 

To be continued... 

Guess you like

Origin blog.csdn.net/weixin_57542177/article/details/126166205