Spring Security OAuth2.0 Authentication and Authorization---Basics

1. Basic concepts

1.1. What is authentication

In the era of mobile Internet, everyone is swiping their mobile phones every day. The commonly used software includes WeChat, Alipay, Toutiao, etc. The following uses WeChat as an example to illustrate the basic concepts related to authentication. Before using WeChat for the first time, you need to register as a WeChat user and enter your account number. and password to log in to WeChat, and the process of entering the account and password to log in to WeChat is authentication.

Why should the system be authenticated?

Authentication is to protect the private data and resources of the system, and users can only access the resources of the system with legal identities.

Authentication : User authentication is the process of judging whether a user's identity is legal. When a user accesses system resources, the system requires verification of the user's identity information. Only when the identity is legal can the user continue to access, and if the identity is illegal, the access is denied. Common user identity authentication methods include: username and password login, QR code login, SMS login, fingerprint authentication, etc.

1.2, what is a session

After the user is authenticated, the user's information can be guaranteed in the session in order to avoid authentication for each operation of the user. A session is a mechanism provided by the system to maintain the login status of the current user. Common methods include session-based and token-based methods.

1.2.1, session-based authentication method

Its interaction process is that after the user authentication is successful, the user-related data is generated on the server side and stored in the session (current session), and the sesssion_id sent to the client is stored in the cookie, so that the user can verify the client with the session_id when requesting Whether there is session data on the server side, so as to complete the legal verification of the user. When the user logs out of the system or the session expires and is destroyed, the session_id of the client will be invalid.

insert image description here

1.2.2. Based on token

Its interaction process is that after the user authentication is successful, the server generates a token and sends it to the client. The client can put it in storage such as cookie or localStorage, and bring the token with each request. The server receives the token and passes the verification. User identity can be confirmed.

insert image description here

  • The session-based authentication method is customized by the Servlet specification. The server needs to occupy memory resources to store session information, and the client needs to support cookies;
  • The token-based method generally does not require the server to store the token, and does not limit the storage method of the client.
  • In today's mobile Internet era, more types of clients need to access the system, and the system is mostly implemented with a separate architecture for the front and back ends, so the token-based approach is more suitable.

1.3. What is authorization

Take WeChat as an example. After successfully logging in to WeChat, users can use WeChat functions, such as sending red envelopes, sending friends circles, adding friends, etc. Users who have not bound a bank card cannot send red envelopes. Those who bind a bank card Only users can send red envelopes. The function of sending red envelopes and the function of sending circles of friends are resources of WeChat, that is, functional resources. Users who have the authority to send red envelopes can use the function of sending red envelopes normally, and only those who have the authority to send circles of friends can use the function of sending friends. The circle function, the process of controlling the user's use of resources according to the user's authority is authorization.

Why authorize?

Authentication is to ensure the legitimacy of the user's identity, and authorization is to divide private data in a finer granularity. Authorization occurs after the authentication is passed, and controls different users to access different resources.

Authorization : Authorization is the process of user authentication to control the user's access to resources according to the user's permissions. If you have access to resources, you can access normally, and if you don't have permission, you will deny access.

1.4. Authorized data model

How to authorize means how to control users' access to resources. First, you need to learn the data model related to authorization.

Authorization can be simply understood as Who performs How operations on What (which), including the following:

Who , that is, the subject (Subject), the subject generally refers to the user, or it can be a program, which needs to access the resources in the system.

What , that is, resources (Resource), such as system menus, pages, buttons, code methods, system product information, system order information, etc. System menus, pages, buttons, and code methods are all system function resources. For web systems, each function resource usually corresponds to a URL; system product information and system order information are all entity resources (data resources), and entity resources are determined by resource type and resource Composition of instances, for example, commodity information is resource type, commodity number 001 is resource instance.

How , permission/permission (Permission), specifies the user's permission to operate the resource, and the permission is meaningless without the resource, such as user query permission, user addition permission, calling permission of a code method, modification permission of the user numbered 001, etc. , through permissions, you can know what operation permissions the user has for which resources.

The relationship between subjects, resources, and permissions is as follows:
insert image description here

The data model related to subjects, resources, and permissions is as follows:

  • Subject (userid, account, password, ...)
  • Resource (resource id, resource name, access address, ...)
  • Permissions (permission id, permission id, permission name, resource id, ...)
  • role (role id, role name, ...)
  • Role and permission relationship (role id, permission id, ...)
  • principal (user) and role relations (userid, roleid, ...)

The relationship between subjects (users), resources, and permissions is as follows:

insert image description here

Usually, in enterprise development, resource and permission tables are combined into one permission table, as follows:

  • Resource (resource id, resource name, access address, ...)
  • Permissions (permission id, permission id, permission name, resource id, ...)

merged into:

  • Permissions (permission id, permission ID, permission name, resource name, resource access address, ...)

The relationship between the modified data models is as follows:
insert image description here

1.5、RBAC

How to achieve authorization? The industry usually implements authorization based on RBAC.

1.5.1, role-based access control

RBAC role-based access control (Role-Based Access Control) is authorized by role. For example, the role of the subject is the general manager, who can query enterprise operation reports and employee salary information. The access control process is as follows:
insert image description here

According to the judgment logic in the above figure, the authorization code can be expressed as follows:

if(主体.hasRole("总经理角色id")){
    
    
	查询工资
}

If the role required to query salary in the above figure changes to general manager and department manager, then you need to modify the judgment logic to "judging whether the user's role is general manager or department manager", and modify the code as follows:

if(主体.hasRole("总经理角色id") || 主体.hasRole("部门经理角色id")){
    
    
	查询工资
}

According to the above example, it is found that when the permission of the role needs to be modified, the relevant code of the authorization needs to be modified, and the scalability of the system is poor.

1.5.2. Resource-based access control

RBAC resource-based access control (Resource-Based Access Control) is authorized by resource (or permission). For example, users must have the permission to query salary to query employee salary information. The access control process is as follows:
insert image description here

According to the judgment in the above figure, the authorization code can be expressed as:

if(主体.hasPermission("查询工资权限标识")){
    
    
	查询工资
}

Advantages: The system is designed to define the permission identifier for salary query. Even if the roles required for salary query are changed to general manager and department manager, there is no need to modify the authorization code, and the system has strong scalability.

2. Session-based authentication method

2.1. Authentication process

The process based on the Session authentication method is that after the user is successfully authenticated, the user-related data is generated on the server side and stored in the session (current session), and the session_id sent to the client is stored in the cookie, so that when the client requests with the session_id It can verify whether there is session data on the server side, so as to complete the legal verification of the user. When the user logs out of the system or the session expires and is destroyed, the session_id of the client is also invalid.

The following figure is a flow chart of the session authentication method:
insert image description here

The session-based authentication mechanism is customized by the Servlet specification. The Servlet container has been implemented, and the user can realize it through the operation method of HttpSession. The following is the operation API related to HttpSession.

method meaning
HttpSession getSession(Boolean create) Get the current HttpSession object
void setAttribute(String name,Object value) Store objects in session
object getAttribute(String name) get object from session
void removeAttribute(String name); remove the object in the session
void invalidate() Invalidate HttpSession

2.2. Create a project

This case project is constructed using maven and implemented using SpringMVC and Servlet3.0.

2.2.1. Create maven project

Create maven project security-springmvc

Introduce the following dependencies as follows, note:

1. Since it is a web project, packaging is set to war
2. Use the tomcat7-maven-plugin plug-in to run the project

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.test.security</groupId>
    <artifactId>security-springmvc</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.0.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
        </dependency>
    </dependencies>
    <build>
        <finalName>security-springmvc</finalName>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.tomcat.maven</groupId>
                    <artifactId>tomcat7-maven-plugin</artifactId>
                    <version>2.2</version>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                    </configuration>
                </plugin>

                <plugin>
                    <artifactId>maven-resources-plugin</artifactId>
                    <configuration>
                        <encoding>utf-8</encoding>
                        <useDefaultDelimiters>true</useDefaultDelimiters>
                        <resources>
                            <resource>
                                <directory>src/main/resources</directory>
                                <filtering>true</filtering>
                                <includes>
                                    <include>**/*</include>
                                </includes>
                            </resource>
                            <resource>
                                <directory>src/main/java</directory>
                                <includes>
                                    <include>**/*.xml</include>
                                </includes>
                            </resource>
                        </resources>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

2.2.2, Spring container configuration

Define ApplicationConfig.java under the config package, which corresponds to the configuration of ContextLoaderListener in web.xml

@Configuration
@ComponentScan(basePackages = "com.test.security.springmvc",excludeFilters = {
    
    @ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class ApplicationConfig {
    
    
	//在此配置除了Controller的其它bean,比如:数据库链接池、事务管理器、业务bean等。
}

2.2.3, servletContext deployment

This case adopts the Servlet3.0 without web.xml method, and defines WebConfig.java under the config package, which corresponds to the DispatcherServlet configuration corresponding to s.

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.test.security.springmvc",includeFilters = {
    
    @ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {
    
    
	//视频解析器
	@Bean
	public InternalResourceViewResolver viewResolver(){
    
    
		InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
		viewResolver.setPrefix("/WEB‐INF/views/");
		viewResolver.setSuffix(".jsp");
		return viewResolver;
	}
}

2.2.4, loading Spring container

Define the Spring container initialization class SpringApplicationInitializer under the init package, which implements the WebApplicationInitializer interface, and loads all implementation classes of the WebApplicationInitializer interface when the Spring container starts.

public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    
    
	
	@Override
	protected Class<?>[] getRootConfigClasses() {
    
    
		return new Class<?>[] {
    
     ApplicationConfig.class };//指定rootContext的配置类
	}
	
	@Override
	protected Class<?>[] getServletConfigClasses() {
    
    
		return new Class<?>[] {
    
     WebConfig.class }; //指定servletContext的配置类
	}
	
	@Override
	protected String[] getServletMappings() {
    
    
		return new String [] {
    
    "/"};
	}
}

SpringApplicationInitializer is equivalent to web.xml. If you use servlet3.0 development, you don’t need to define web.xml again. ApplicationConfig.class corresponds to the application-context.xml of the following configuration, and WebConfig.class corresponds to the springmvc.xml and web.xml of the following configuration content reference

<web‐app>
	<listener>
		<listener‐class>org.springframework.web.context.ContextLoaderListener</listener‐class>
	</listener>
	<context‐param>
		<param‐name>contextConfigLocation</param‐name>
		<param‐value>/WEB‐INF/application‐context.xml</param‐value>
	</context‐param>
	
	<servlet>
		<servlet‐name>springmvc</servlet‐name>
		<servlet‐class>org.springframework.web.servlet.DispatcherServlet</servlet‐class>
		<init‐param>
			<param‐name>contextConfigLocation</param‐name>
			<param‐value>/WEB‐INF/spring‐mvc.xml</param‐value>
		</init‐param>
		<load‐on‐startup>1</load‐on‐startup>
	</servlet>
	<servlet‐mapping>
		<servlet‐name>springmvc</servlet‐name>
		<url‐pattern>/</url‐pattern>
	</servlet‐mapping>
</web‐app>

2.3. Realize the authentication function

2.3.1. Authentication page

Define the authentication page login.jsp under webapp/WEB-INF/views. This case is just to test the authentication process. There is no css style added to the page. The page can be implemented by filling in the user name and password. When the login is triggered, the form information will be submitted to /login, and the content as follows:

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="utf-8" %>
<html>
<head>
    <title>用户登录</title>
</head>
<body>
<form action="login" method="post">
    用户名:<input type="text" name="username"><br>&nbsp;&nbsp;&nbsp;码:
    <input type="password" name="password"><br>
    <input type="submit" value="登录">
</form>
</body>
</html>

Add the following configuration in WebConfig to direct /directly to the login.jsp page:

@Override
public void addViewControllers(ViewControllerRegistry registry) {
    
    
	registry.addViewController("/").setViewName("login");
}

Start the project, access the / path address, and test

insert image description here

2.3.2. Authentication interface

The user enters the authentication page, enters the account number and password, clicks to log in, and requests /login for identity authentication.

(1) Define the authentication interface, which is used to verify the incoming username and password, and return the user's detailed information if successful, otherwise throw an error exception:

/**
* 认证服务
*/
public interface AuthenticationService {
    
    
	/**
	* 用户认证
	* @param authenticationRequest 用户认证请求
	* @return 认证成功的用户信息
	*/
	UserDto authentication(AuthenticationRequest authenticationRequest);
}

Authentication request structure:

@Data
public class AuthenticationRequest {
    
    
	/**
	* 用户名
	*/
	private String username;
	/**
	* 密码
	*/
	private String password;
}

The user details returned after successful authentication, that is, the information of the currently logged-in user:

/**
* 当前登录用户信息
*/
@Data
@AllArgsConstructor
public class UserDto {
    
    
	private String id;
	private String username;
	private String password;
	private String fullname;
	private String mobile;
}

(2) The authentication implementation class, which searches for user information according to the user name and verifies the password. Here, two users are simulated:

@Service
public class AuthenticationServiceImpl implements  AuthenticationService{
    
    
    /**
     * 用户认证,校验用户身份信息是否合法
     *
     * @param authenticationRequest 用户认证请求,账号和密码
     * @return 认证成功的用户信息
     */
    @Override
    public UserDto authentication(AuthenticationRequest authenticationRequest) {
    
    
        //校验参数是否为空
        if(authenticationRequest == null
            || StringUtils.isEmpty(authenticationRequest.getUsername())
            || StringUtils.isEmpty(authenticationRequest.getPassword())){
    
    
            throw new RuntimeException("账号和密码为空");
        }
        //根据账号去查询数据库,这里测试程序采用模拟方法
        UserDto user = getUserDto(authenticationRequest.getUsername());
        //判断用户是否为空
        if(user == null){
    
    
            throw new RuntimeException("查询不到该用户");
        }
        //校验密码
        if(!authenticationRequest.getPassword().equals(user.getPassword())){
    
    
            throw new RuntimeException("账号或密码错误");
        }
        //认证通过,返回用户身份信息
        return user;
    }
    //根据账号查询用户信息
    private UserDto getUserDto(String userName){
    
    
        return userMap.get(userName);
    }
    
    //用户信息
    private Map<String,UserDto> userMap = new HashMap<>();
    {
    
    
        userMap.put("zhangsan",new UserDto("1010","zhangsan","123","张三","133443",authorities1));
        userMap.put("lisi",new UserDto("1011","lisi","456","李四","144553",authorities2));
    }
}

(3) Log in to the Controller and process the /login request. It calls the AuthenticationService to complete the authentication and returns the prompt message of the login result:

@RestController
public class LoginController {
    
    

	@Autowired
	private AuthenticationService authenticationService;
	
	/**
	* 用户登录
	* @param authenticationRequest 登录请求
	* @return
	*/
	@PostMapping(value = "/login",produces = {
    
    "text/plain;charset=UTF‐8"})
	public String login(AuthenticationRequest authenticationRequest){
    
    
		UserDetails userDetails = authenticationService.authentication(authenticationRequest);
		return userDetails.getFullname() + " 登录成功";
	}
}

(4) Test
Start the project, access / path address, and test

2.4. Realize the session function

A session means that after a user logs in to the system, the system will remember the user's login status, and he can continue to operate the system until he logs out of the system.

The purpose of authentication is to protect system resources. Every time a resource is accessed, the system must know who is accessing the resource in order to legally intercept the request. Therefore, after the authentication is successful, the user information of the successfully authenticated user is generally put into the Session. In subsequent requests, the system can obtain the current user from the Session, and implement the session mechanism in this way.

(1) Increase session control
First define a SESSION_USER_KEY in UserDto as the key for storing login user information in the Session.

public static final String SESSION_USER_KEY = "_user";

Then modify the LoginController, after the authentication is successful, put the user information into the current session. And add a user logout method, and set the session to be invalid when logging out.

@RestController
public class LoginController {
    
    

	@Autowired
	private AuthenticationService authenticationService;
	
	/**
	* 用户登录
	* @param authenticationRequest 登录请求
	* @param session http会话
	* @return
	*/
	@PostMapping(value = "/login",produces = "text/plain;charset=utf‐8")
	public String login(AuthenticationRequest authenticationRequest, HttpSession session){
    
    
		UserDto userDto = authenticationService.authentication(authenticationRequest);
		//用户信息存入session
		session.setAttribute(UserDto.SESSION_USER_KEY,userDto);
		return userDto.getUsername() + "登录成功";
	}
	
	@GetMapping(value = "logout",produces = "text/plain;charset=utf‐8")
	public String logout(HttpSession session){
    
    
		session.invalidate();
		return "退出成功";
	}
}

(2) Add test resources
Modify LoginController and add test resource 1, which obtains the current login user from the current session and returns prompt information to the foreground.

/**
* 测试资源1
* @param session
* @return
*/
@GetMapping(value = "/r/r1",produces = {
    
    "text/plain;charset=UTF‐8"})
public String r1(HttpSession session){
    
    
	String fullname = null;
	Object userObj = session.getAttribute(UserDto.SESSION_USER_KEY);
	if(userObj != null){
    
    
		fullname = ((UserDto)userObj).getFullname();
	}else{
    
    
		fullname = "匿名";
	}
	return fullname + " 访问资源1";
}

2.5. Realize the authorization function

Now we have completed the verification of user identity credentials and the status of login, and we also know how to obtain the information of the currently logged in user (obtained from the Session). Next, the user needs to be authorized to access the system, that is, the following needs to be done Function:

  • Anonymous user (not logged in user) access blocking: prohibit anonymous users from accessing certain resources.
  • Logged-in user access interception: Determine whether certain resources can be accessed according to the user's permissions.

(1) Add permission data
In order to realize this function, we need to add a permission attribute in UserDto, which is used to indicate the permission owned by the logged-in user, and modify the construction method of UserDto at the same time.

@Data
@AllArgsConstructor
public class UserDto {
    
    
    public static final String SESSION_USER_KEY = "_user";
    //用户身份信息
    private String id;
    private String username;
    private String password;
    private String fullname;
    private String mobile;
    /**
     * 用户权限
     */
    private Set<String> authorities;
}

And initialize the permissions for the simulated user in AuthenticationServiceImpl, among which Zhang San gave the p1 permission, and Li Si gave the p2 permission.

//用户信息
private Map<String,UserDto> userMap = new HashMap<>();
{
    
    
    Set<String> authorities1 = new HashSet<>();
    authorities1.add("p1");//这个p1我们人为让它和/r/r1对应
    Set<String> authorities2 = new HashSet<>();
    authorities2.add("p2");//这个p2我们人为让它和/r/r2对应
    userMap.put("zhangsan",new UserDto("1010","zhangsan","123","张三","133443",authorities1));
    userMap.put("lisi",new UserDto("1011","lisi","456","李四","144553",authorities2));
}

(2) Add test resources
We want to realize that different users can access different resources, the premise is that there must be multiple resources, so add test resource 2 in LoginController.

/**
* 测试资源2
* @param session
* @return
*/
@GetMapping(value = "/r/r2",produces = {
    
    "text/plain;charset=UTF‐8"})
public String r2(HttpSession session){
    
    
	String fullname = null;
	Object userObj = session.getAttribute(UserDto.SESSION_USER_KEY);
	if(userObj != null){
    
    
		fullname = ((UserDto)userObj).getFullname();
	}else{
    
    
		fullname = "匿名";
	}
	return fullname + " 访问资源2";
}

(3) Realize the authorization interceptor
Define the SimpleAuthenticationInterceptor interceptor under the interceptor package to realize authorization interception:
1. Verify whether the user is logged in
2. Verify whether the user has operation authority

@Component
public class SimpleAuthenticationInterceptor implements HandlerInterceptor {
    
    

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    
        //在这个方法中校验用户请求的url是否在用户的权限范围内
        //取出用户身份信息
        Object object = request.getSession().getAttribute(UserDto.SESSION_USER_KEY);
        if(object == null){
    
    
            //没有认证,提示登录
            writeContent(response,"请登录");
        }
        UserDto userDto = (UserDto) object;
        //请求的url
        String requestURI = request.getRequestURI();
        if( userDto.getAuthorities().contains("p1") && requestURI.contains("/r/r1")){
    
    
            return true;
        }
        if( userDto.getAuthorities().contains("p2") && requestURI.contains("/r/r2")){
    
    
            return true;
        }
        writeContent(response,"没有权限,拒绝访问");
        return false;
    }

    //响应信息给客户端
    private void writeContent(HttpServletResponse response, String msg) throws IOException {
    
    
        response.setContentType("text/html;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.print(msg);
        writer.close();
        response.resetBuffer();
    }
}

Configure the interceptor in WebConfig, the resource matching /r/** is a protected system resource, and the request to access the resource enters the SimpleAuthenticationInterceptor interceptor.

@Configuration//就相当于springmvc.xml文件
@EnableWebMvc
@ComponentScan(basePackages = "com.test.security.springmvc"
        ,includeFilters = {
    
    @ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {
    
    

    @Autowired
    SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    
    
        registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns("/r/**");
    }
}

(4) Test
If you are not logged in, both /r/r1 and /r/r2 will prompt "Please log in first".
When Zhang San logs in, since Zhang San has p1 permission, he can access /r/r1, but Zhang San does not have p2 permission, and when he accesses /r/r2, he prompts "Insufficient permission".
When Li Si logs in, because Li Si has p2 permission, he can access /r/r2, but Li Si does not have p1 permission, and when he accesses /r/r1, he prompts "Insufficient Permission".
The test results are all in line with the expected results.

2.6 Summary

The session-based authentication method is a common authentication method, and there are still many systems in use so far. In this section, we use Spring mvc technology to implement it simply, aiming to let everyone understand the functional meaning and implementation routines of user authentication, authorization, and session, that is, what do they do? What should I do?

In formal production projects, we often consider using third-party security frameworks (such as spring security, shiro and other security frameworks) to implement authentication and authorization functions, because this can improve productivity to a certain extent and improve software standardization. In addition, these frameworks often The scalability is considered very comprehensive. But the shortcomings are also very obvious. In order to improve the scope of support, these general components will add a lot of functions that we may not need, and the structure will be relatively abstract. If we don't understand it enough, once a problem occurs, it will be difficult to locate.

Three, Spring Security quick start

3.1. Introduction to Spring Security

Spring Security is a security framework that can provide declarative security access control solutions for Spring-based enterprise application systems. Because it is a member of the Spring ecosystem, it is constantly revised and upgraded along with the entire Spring ecosystem. It is very simple to add spring security to the spring boot project. Using Spring Security reduces the need to write a lot of duplication for enterprise system security control The code works.

3.2. Create a project

3.2.1. Create maven project

Create a maven project security-spring-security
to introduce the following dependencies:

<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring‐security‐web</artifactId>
	<version>5.1.4.RELEASE</version>
</dependency>
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring‐security‐config</artifactId>
	<version>5.1.4.RELEASE</version>
</dependency>

3.2.2, Spring container configuration

@Configuration
@ComponentScan(basePackages = "com.test.security.springmvc",excludeFilters = {
    
    @ComponentScan.Filter(type = FilterType.ANNOTATION,value =
Controller.class)})
public class ApplicationConfig {
    
    
	//在此配置除了Controller的其它bean,比如:数据库链接池、事务管理器、业务bean等。
}

3.2.3, Servlet Context configuration

@Configuration//就相当于springmvc.xml文件
@EnableWebMvc
@ComponentScan(basePackages = "com.test.security.springmvc"
        ,includeFilters = {
    
    @ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {
    
    

    //视频解析器
    @Bean
    public InternalResourceViewResolver viewResolver(){
    
    
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/view/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }  
}

3.2.4, loading Spring container

Define the Spring container initialization class SpringApplicationInitializer under the init package, which implements the WebApplicationInitializer interface, and loads all implementation classes of the WebApplicationInitializer interface when the Spring container starts.

public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    
    

    //spring容器,相当于加载 applicationContext.xml
    @Override
    protected Class<?>[] getRootConfigClasses() {
    
    
        return new Class[]{
    
    ApplicationConfig.class};
    }

    //servletContext,相当于加载springmvc.xml
    @Override
    protected Class<?>[] getServletConfigClasses() {
    
    
        return new Class[]{
    
    WebConfig.class};
    }

    //url-mapping
    @Override
    protected String[] getServletMappings() {
    
    
        return new String[]{
    
    "/"};
    }
}

3.3. Authentication

3.3.1. Authentication page

springSecurity provides authentication pages by default, no additional development is required.

3.3.2, security configuration

Spring security provides authentication functions such as username and password login, logout, and session management, which can be used only by configuration.

1) Define WebSecurityConfig under the config package. The content of the security configuration includes: user information, password encoder, and security interception mechanism.

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    
	//配置用户信息服务
	@Bean
	public UserDetailsService userDetailsService() {
    
    
		InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
		manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
		manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
		return manager;
	}
	
	@Bean
	public PasswordEncoder passwordEncoder() {
    
    
		return NoOpPasswordEncoder.getInstance();
	}
	
	//配置安全拦截机制
	@Override
	protected void configure(HttpSecurity http) throws Exception {
    
    
		http.authorizeRequests()
		.antMatchers("/r/**").authenticated()//url匹配/r/**的资源,经过认证后才能访问。
		.anyRequest().permitAll()//其他url完全开放。
		.and()
		.formLogin().successForwardUrl("/login‐success");//支持form表单认证,认证成功后转向/login-success。
	}
}

In the userDetailsService() method, we return a UserDetailsService to the spring container, which Spring Security will use to obtain user information. We temporarily use the InMemoryUserDetailsManager implementation class, and create two users, zhangsan and lisi, in it, and set passwords and permissions

In configure(), we set security interception rules through HttpSecurity

2) Load WebSecurityConfig
and modify the getRootConfigClasses() method of SpringApplicationInitializer to add WebSecurityConfig.class:

@Override
protected Class<?>[] getRootConfigClasses() {
    
    
	return new Class<?>[] {
    
     ApplicationConfig.class, WebSecurityConfig.class};
}

3.3.3, Spring Security initialization

  • If the current environment does not use Spring or Spring MVC, you need to pass WebSecurityConfig (Spring Security configuration class) into the superclass to ensure that the configuration is obtained and the spring context is created.

  • On the contrary, if spring is already used in the current environment, we should register Spring Security in the existing springContext (we loaded WebSecurityConfig to rootcontext in the previous step), and this method can do nothing.

Define SpringSecurityApplicationInitializer under the init package:

public class SpringSecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
    
    
	public SpringSecurityApplicationInitializer() {
    
    
		//super(WebSecurityConfig.class);
	}
}

3.3.4. Default root path request

Add the default request root path to jump to /login in WebConfig.java, this url provides spring security:

//默认Url根路径跳转到/login,此url为spring security提供
@Override
public void addViewControllers(ViewControllerRegistry registry) {
    
    
	registry.addViewController("/").setViewName("redirect:/login");
}

3.3.5. Authentication success page

In the security configuration, if the authentication is successful, it will jump to /login-success, the code is as follows:

//配置安全拦截机制
@Override
protected void configure(HttpSecurity http) throws Exception {
    
    
	http.authorizeRequests()
	.antMatchers("/r/**").authenticated()
	.anyRequest().permitAll()
	.and()
	.formLogin().successForwardUrl("/login‐success");
}

Spring security supports form form authentication, and turns to /login-success after successful authentication.
Define /login-success in LoginController:

@RestController
public class LoginController {
    
    

    @RequestMapping(value = "/login-success",produces = {
    
    "text/plain;charset=UTF-8"})
    public String loginSuccess(){
    
    
        return " 登录成功";
    } 
}

3.3.6. Test

(1) Start the project, visit http://localhost:8080/security-spring-security/ path address

  • The page will jump to /login according to the addViewControllers configuration rules in WebConfig, and /login is the login page provided by pring Security.

(2) Login

  • 1. Enter wrong username and password
  • 2. Enter the correct user name and password, and the login is successful

(3) Exit

  • 1. Request /logout to exit
  • 2. After logging out and accessing resources, it will automatically jump to the login page

3.4. Authorization

To implement authorization, it is necessary to intercept and verify the user's access to verify whether the user's authority can operate the specified resource. Spring Security provides an authorization implementation method by default.

Add /r/r1 or /r/r2 in LoginController

@RestController
public class LoginController {
    
    

    @RequestMapping(value = "/login-success",produces = {
    
    "text/plain;charset=UTF-8"})
    public String loginSuccess(){
    
    
        return " 登录成功";
    }

    /**
     * 测试资源1
     * @return
     */
    @GetMapping(value = "/r/r1",produces = {
    
    "text/plain;charset=UTF-8"})
    public String r1(){
    
    
        return " 访问资源1";
    }

    /**
     * 测试资源2
     * @return
     */
    @GetMapping(value = "/r/r2",produces = {
    
    "text/plain;charset=UTF-8"})
    public String r2(){
    
    
        return " 访问资源2";
    }
}

Configure authorization rules in the security configuration class WebSecurityConfig.java:

//安全拦截机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
    
    
   http.authorizeRequests()
          .antMatchers("/r/r1").hasAuthority("p1")
          .antMatchers("/r/r2").hasAuthority("p2")
          .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
          .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
          .and()
          .formLogin()//允许表单登录
          .successForwardUrl("/login-success");//自定义登录成功的页面地址

}
  • .antMatchers("/r/r1").hasAuthority("p1") means: the url to access the /r/r1 resource needs to have the p1 authority.

  • .antMatchers(“/r/r2”).hasAuthority(“p2”) means: the url to access the /r/r2 resource needs to have the p2 authority.

Test:
1. Log in successfully
2. Access /r/r1 and /r/r2. If you have permission, you can access normally, otherwise return 403 (access denied)

3.5 Summary

Through a quick start, we use Spring Security to implement authentication and authorization. Spring Security provides authentication methods based on account numbers and passwords. Through security configuration, request interception and authorization functions can be realized. Spring Security can do more than these.

4. Detailed explanation of Spring Security application

4.1. Integrate SpringBoot

4.1.1. Introduction to Spring Boot

Spring Boot is a rapid development framework for Spring. Based on the Spring 4.0 design, using Spring Boot to develop can avoid some tedious project construction and configuration. At the same time, it integrates a large number of common frameworks, quickly imports dependent packages, and avoids conflicts between dependent packages. Basically, the commonly used development frameworks support Spring Boot development, such as MyBatis, Dubbo, etc., especially the Spring family, such as: Spring cloud, Spring mvc, Spring security, etc. Using Spring Boot development can greatly increase productivity, so Spring Boot usage is very high.

Next, let's talk about how to develop Spring Security applications through Spring Boot. Spring Boot provides spring-boot-starter-security for developing Spring Security applications.

4.1.2. Create maven project

1) Create maven project security-spring-boot
2) Introduce the following dependencies:

<dependencies>
    <!-- 以下是>spring boot依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- 以下是>spring security依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>


    <!-- 以下是jsp依赖-->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <scope>provided</scope>
    </dependency>
    <!--jsp页面使用jstl标签 -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <scope>provided</scope>
    </dependency>
    <!--用于编译jsp -->
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-jasper</artifactId>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
</dependencies>

4.1.3, spring container configuration

The SpringBoot project startup will automatically scan all the beans under the package where the startup class is located, and load them into the spring container.
1) Spring Boot configuration file
Add application.properties under resources, the content is as follows:

server.port=8080
server.servlet.context‐path=/security‐springboot
spring.application.name = security‐springboot

2) Spring Boot startup class

@SpringBootApplication
public class SecuritySpringBootApp {
    
    
	public static void main(String[] args) {
    
    
		SpringApplication.run(SecuritySpringBootApp.class, args);
	}
}

4.1.4, Servlet Context configuration

Due to the automatic assembly mechanism of Spring boot starter, there is no need to use @EnableWebMvc and @ComponentScan here, and the WebConfig is as follows

@Configuration//就相当于springmvc.xml文件
public class WebConfig implements WebMvcConfigurer {
    
    

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
    
    
        registry.addViewController("/").setViewName("redirect:/login-view");

    }
}

The video parser is configured in application.properties

spring.mvc.view.prefix=/WEB‐INF/views/
spring.mvc.view.suffix=.jsp

4.1.5. Security configuration

Due to the automatic assembly mechanism of Spring boot starter, there is no need to use @EnableWebSecurity here, and the content of WebSecurityConfig is as follows

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    

	//配置用户信息服务
	@Bean
	public UserDetailsService userDetailsService() {
    
    
		InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
		manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
		manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
		return manager;
	}
	
	@Bean
	public PasswordEncoder passwordEncoder() {
    
    
		return NoOpPasswordEncoder.getInstance();
	}
	
	//配置安全拦截机制
	@Override
	protected void configure(HttpSecurity http) throws Exception {
    
    
		http.authorizeRequests()
		.antMatchers("/r/**").authenticated()
		.anyRequest().permitAll()
		.and()
		.formLogin().successForwardUrl("/login‐success");
	}
}

4.1.6. Test

@RestController
public class LoginController {
    
    

    @RequestMapping(value = "/login-success",produces = {
    
    "text/plain;charset=UTF-8"})
    public String loginSuccess(){
    
    
        return " 登录成功";
    }

    /**
     * 测试资源1
     * @return
     */
    @GetMapping(value = "/r/r1",produces = {
    
    "text/plain;charset=UTF-8"})
    public String r1(){
    
    
        return " 访问资源1";
    }

    /**
     * 测试资源2
     * @return
     */
    @GetMapping(value = "/r/r2",produces = {
    
    "text/plain;charset=UTF-8"})
    public String r2(){
    
    
        return " 访问资源2";
    }
}

(1) Start the project, visit http://localhost:8080/security-springboot/ path address

  • The page will jump to /login according to the addViewControllers configuration rules in WebConfig, and /login is the login page provided by pring Security.

(2) Login

  • 1. Enter wrong username and password
  • 2. Enter the correct user name and password, and the login is successful

(3) Exit

  • 1. Request /logout to exit
  • 2. After logging out and accessing resources, it will automatically jump to the login page

4.2. Working principle

4.2.1. Structure overview

The problem that Spring Security solves is security access control , and the security access control function is actually to intercept all requests entering the system, and verify whether each request can access the resources it expects. According to the learning of the previous knowledge, it can be realized by technologies such as Filter or AOP. Spring Security's protection of web resources is realized by Filter, so start with this Filter and gradually deepen the principle of Spring Security.

When Spring Security is initialized, a Servlet filter named SpringSecurityFilterChain will be created, of type org.springframework.security.web.FilterChainProxy, which implements javax.servlet.Filter, so external requests will pass through this class, as shown below Spring Security filter chain structure diagram:

insert image description here

FilterChainProxy is a proxy. What really works is the Filters contained in SecurityFilterChain in FilterChainProxy. At the same time, these Filters are managed by Spring as beans. They are the core of Spring Security and have their own responsibilities, but they do not directly handle user authentication. It does not directly process user authorization, but hands them over to AuthenticationManager and AccessDecisionManager for processing . The figure below is a UML diagram of FilterChainProxy related classes.

insert image description here

The realization of spring Security function is mainly completed by a series of filter chains.

insert image description here

The following describes the main filters and their functions in the filter chain:

  • SecurityContextPersistenceFilter This Filter is the entry and exit of the entire interception process (that is, the first and last interceptor). It will get the SecurityContext from the configured SecurityContextRepository at the beginning of the request, and then set it to the SecurityContextHolder. After the request is completed, save the SecurityContext held by the SecurityContextHolder to the configured SecurityContextRepository, and clear the SecurityContext held by the securityContextHolder;

  • UsernamePasswordAuthenticationFilter is used to handle authentication from form submissions. The form must provide the corresponding user name and password, and there are also AuthenticationSuccessHandler and AuthenticationFailureHandler for processing after successful or failed login, which can be changed according to requirements;

  • FilterSecurityInterceptor is used to protect web resources, using AccessDecisionManager to authorize access to the current user, which has been introduced in detail earlier;

  • ExceptionTranslationFilter can catch all exceptions from FilterChain and handle them. But it will only handle two types of exceptions: AuthenticationException and AccessDeniedException, and it will continue to throw other exceptions.

4.2.2. Authentication process

4.2.2.1. Authentication process

insert image description here

Let's dissect the authentication process in detail:

  • The user name and password submitted by the user are obtained by the UsernamePasswordAuthenticationFilter filter in the SecurityFilterChain and encapsulated as a request Authentication, which is usually the implementation class UsernamePasswordAuthenticationToken.

  • Then the filter submits Authentication to AuthenticationManager for authentication

  • After successful authentication, the AuthenticationManager identity manager returns an Authentication instance filled with information (including the above mentioned permission information, identity information, details information, but the password is usually removed).

  • The SecurityContextHolder security context container sets the Authentication filled with information in step 3 into it through the SecurityContextHolder.getContext().setAuthentication(…) method.

    • It can be seen that the AuthenticationManager interface (authentication manager) is the core interface related to authentication and the starting point for initiating authentication. Its implementation class is ProviderManager. Spring Security supports multiple authentication methods, so ProviderManager maintains a List list, storing multiple authentication methods, and the final actual authentication work is done by AuthenticationProvider. We know that the corresponding AuthenticationProvider implementation class of the web form is DaoAuthenticationProvider, and it maintains a UserDetailsService inside which is responsible for obtaining UserDetails. Finally, AuthenticationProvider fills UserDetails into Authentication.

The general relationship between the authentication core components is as follows:

insert image description here

4.2.2.2、AuthenticationProvider

Through the previous Spring Security authentication process, we know that the authentication manager (AuthenticationManager) entrusts the AuthenticationProvider to complete the authentication work.

AuthenticationProvider is an interface defined as follows:

public interface AuthenticationProvider {
    
    
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
	boolean supports(Class<?> var1);
}

The authenticate() method defines the implementation process of authentication. Its parameter is an Authentication, which contains the user and password submitted by the logged-in user. The return value is also an Authentication, which is generated after reassembling the user's permissions and other information after the authentication is successful.

Spring Security maintains a List list, which stores multiple authentication methods, and different authentication methods use different AuthenticationProviders. For example, there are many examples of using AuthenticationProvider1 when logging in with a username and password, using AuthenticationProvider2 when logging in with a text message, and so on.

Each AuthenticationProvider needs to implement the supports() method to indicate the authentication method it supports. For example, if we use form authentication, Spring Security will generate UsernamePasswordAuthenticationToken when submitting a request. It is an Authentication that encapsulates the username and password information submitted by the user. . And correspondingly, which AuthenticationProvider will handle it?

We found the following code in the base class AbstractUserDetailsAuthenticationProvider of DaoAuthenticationProvider:

public boolean supports(Class<?> authentication) {
    
    
	return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}

That is to say, when the web form submits the username and password, Spring Security is handled by DaoAuthenticationProvider.

Finally, let's take a look at the structure of Authentication (authentication information), which is an interface, and the UsernamePasswordAuthenticationToken we mentioned earlier is one of its implementations:

//Authentication是spring security包中的接口,直接继承自Principal类,而Principal是位于 java.security包中的。它是表示着一个抽象主体身份,任何主体都有一个名称,因此包含一个getName()方法。
public interface Authentication extends Principal, Serializable {
    
     
	//权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系列字符串。
	Collection<? extends GrantedAuthority> getAuthorities(); 
	
	//凭证信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
	Object getCredentials(); 
	
	//细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值。
	Object getDetails(); 
	
	//身份信息,大部分情况下返回的是UserDetails接口的实现类,UserDetails代表用户的详细信息,那从Authentication中取出来的UserDetails就是当前登录用户信息,它也是框架中的常用接口之一。
	Object getPrincipal(); 
	
	boolean isAuthenticated();
	
	void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

4.2.2.3、UserDetailsService

1) Get to know UserDetailsService
Now we know that DaoAuthenticationProvider handles the authentication logic of web forms. After successful authentication, an Authentication (implemented by UsernamePasswordAuthenticationToken) is obtained, which contains identity information (Principal). This identity information is an Object, which can be forced into a UserDetails object in most cases.

DaoAuthenticationProvider contains a UserDetailsService instance, which is responsible for extracting user information UserDetails (including password) according to the username, and then DaoAuthenticationProvider will compare whether the user password extracted by UserDetailsService matches the password submitted by the user as the key basis for successful authentication, so you can pass A custom UserDetailsService is exposed as a spring bean to define custom authentication.

public interface UserDetailsService {
    
    
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

Many people confuse the responsibilities of DaoAuthenticationProvider and UserDetailsService. In fact, UserDetailsService is only responsible for loading user information from a specific place (usually a database), nothing more. The DaoAuthenticationProvider has a greater responsibility, it completes the complete authentication
process, and at the same time fills UserDetails into Authentication.

It has been mentioned above that UserDetails is user information, let's take a look at its true colors:

public interface UserDetails extends Serializable {
    
    
	Collection<? extends GrantedAuthority> getAuthorities();
	String getPassword();
	String getUsername();
	boolean isAccountNonExpired();
	boolean isAccountNonLocked();
	boolean isCredentialsNonExpired();
	boolean isEnabled();
}

It is very similar to the Authentication interface, for example, they both have username and authorities. Authentication's getCredentials() and UserDetails' getPassword() need to be treated differently. The former is the password credential submitted by the user, while the latter is the password actually stored by the user. Authentication is actually a comparison of the two. The getAuthorities() in Authentication is actually formed by passing the getAuthorities() of UserDetails. Remember the getDetails() method in the Authentication interface? Among them, UserDetails user details are filled after AuthenticationProvider authentication.

By implementing UserDetailsService and UserDetails, we can complete the expansion of user information acquisition methods and user information fields.

InMemoryUserDetailsManager (memory authentication) and JdbcUserDetailsManager (jdbc authentication) provided by Spring Security are the implementation classes of UserDetailsService. The main difference is that users are loaded from memory or from the database.

2) test

Custom UserDetailsService

@Service
public class SpringDataUserDetailsService implements UserDetailsService {
    
    

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
		//登录账号
		System.out.println("username="+username);
		//根据账号去数据库查询...
		//这里暂时使用静态数据
		UserDetails userDetails = User.withUsername(username).password("123").authorities("p1").build();
		return userDetails;
	}
}

Shield the definition of UserDetailsService in the security configuration class

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    //定义用户信息服务(查询用户信息)
/*
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return manager;
    }
*/

}

Restart the project, request authentication, and the loadUserByUsername method of SpringDataUserDetailsService is called to query user information.

4.2.2.4、PasswordEncoder

1) Get to know PasswordEncoder
DaoAuthenticationProvider After the authentication processor obtains UserDetails through UserDetailsService, how does it compare with the password in the request Authentication?

Here, in order to adapt to a variety of encryption types, Spring Security has made an abstraction. DaoAuthenticationProvider performs password comparison through the matches method of the PasswordEncoder interface, and the specific password comparison details depend on the implementation:

public interface PasswordEncoder {
    
    

	String encode(CharSequence var1);

	boolean matches(CharSequence var1, String var2);
	
	default boolean upgradeEncoding(String encodedPassword) {
    
    
		return false;
	}
}

And Spring Security provides many built-in PasswordEncoders, which can be used out of the box. To use a certain PasswordEncoder, you only need to make the following statement, as follows:

@Bean
public PasswordEncoder passwordEncoder() {
    
    
	return NoOpPasswordEncoder.getInstance();
}

NoOpPasswordEncoder adopts the string matching method and does not encrypt and compare passwords. The password comparison process is as follows:

  • 1. The user enters the password (in plain text)

  • 2. DaoAuthenticationProvider obtains UserDetails (which stores the correct password of the user)

  • 3. DaoAuthenticationProvider uses PasswordEncoder to verify the input password and the correct password. If the passwords are consistent, the verification passes, otherwise the verification fails.

The verification rule of NoOpPasswordEncoder compares the input password with the correct password in UserDetails. If the string content is consistent, the verification passes, otherwise the verification fails.

It is recommended to use BCryptPasswordEncoder, Pbkdf2PasswordEncoder, SCryptPasswordEncoder, etc. in actual projects. If you are interested, you can take a look at the specific implementations of these PasswordEncoders.

2) Use BCryptPasswordEncoder
1. Configure BCryptPasswordEncoder
to define in the security configuration class:

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

The test found that the authentication failed, prompting: Encoded password does not look like BCrypt.
Reason: Since the original password (for example: 123) is stored in UserDetails, it is not in BCrypt format. Track the code on line 33 of DaoAuthenticationProvider to check the content in userDetails, and track the code on line 38 to check the type of PasswordEncoder.

2. Test BCrypt
Test the method of BCrypt encryption and verification through the following code
Add dependencies:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring‐boot‐starter‐test</artifactId>
	<scope>test</scope>
</dependency>

Write a test method:

@RunWith(SpringRunner.class)
public class TestBCrypt {
    
    

    @Test
    public void testBCrypt(){
    
    

        //对密码进行加密
        String hashpw = BCrypt.hashpw("123", BCrypt.gensalt());
        System.out.println(hashpw);

        //校验密码
        boolean checkpw = BCrypt.checkpw("123", "$2a$10$NlBC84MVb7F95EXYTXwLneXgCca6/GipyWR5NHm8K0203bSQMLpvm");  
        System.out.println(checkpw);
    }
}

3. Modify the security configuration class
to change the original password in UserDetails to BCrypt format

//配置用户信息服务
@Bean
public UserDetailsService userDetailsService() {
    
    
	InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
	manager.createUser(User.withUsername("zhangsan").password("$2a$10$1b5mIkehqv5c4KRrX9bUj.A4Y2hug3IGCnMCL5i4RpQrYV12xNKye").authorities("p1").build());
	return manager;
}

The passwords stored in the database in the actual project are not original passwords, but encrypted passwords.

4.2.3. Authorization process

4.2.3.1. Authorization process

Through the quick start, we know that Spring Security can authorize and protect web requests through http.authorizeRequests(). Spring Security uses standard Filter to establish the interception of web requests, and finally realizes authorized access to resources.

The authorization process of Spring Security is as follows:

insert image description here

Analyze the authorization process:

  • Intercepting requests, the authenticated user's access to protected web resources will be intercepted by the subclass of FilterSecurityInterceptor in SecurityFilterChain.

  • Obtain the resource access policy, FilterSecurityInterceptor will obtain the collection of permissions required to access the current resource from DefaultFilterInvocationSecurityMetadataSource, a subclass of SecurityMetadataSource.

    • SecurityMetadataSource is actually the abstraction of the read access strategy, and the read content is actually the access rule we configured. The read access strategy is as follows:
//安全拦截机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
    
    
   http.authorizeRequests()
          .antMatchers("/r/r1").hasAuthority("p1")
          .antMatchers("/r/r2").hasAuthority("p2")
          .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
          .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
          .and()
          .formLogin()//允许表单登录
          .successForwardUrl("/login-success");//自定义登录成功的页面地址

}
  • Finally, FilterSecurityInterceptor will call AccessDecisionManager to make an authorization decision. If the decision is passed, access to the resource is allowed, otherwise access is prohibited.
    • The core interface of AccessDecisionManager (access decision manager) is as follows:
public interface AccessDecisionManager {
    
    
	/**
	* 通过传递的参数来决定用户是否有访问对应受保护资源的权限
	*/
	void decide(Authentication authentication , Object object, Collection<ConfigAttribute> configAttributes ) throws AccessDeniedException, InsufficientAuthenticationException;
	//略..
}

Here we focus on the parameters of decide:

  • authentication: the identity of the visitor who wants to access the resource
  • object: the protected resource to be accessed, the web request corresponds to FilterInvocation
  • configAttributes: is the access policy of protected resources, obtained through SecurityMetadataSource.

The decide interface is used to identify whether the current user has access to the corresponding protected resources.

4.2.3.2. Authorization decision

AccessDecisionManager uses voting to determine whether protected resources can be accessed.

insert image description here

As can be seen from the above figure, a series of AccessDecisionVoter contained in AccessDecisionManager will be used to vote on whether Authentication has the right to access the protected object, and AccessDecisionManager makes the final decision based on the voting results.

AccessDecisionVoter is an interface, which defines three methods, and the specific structure is as follows.

public interface AccessDecisionVoter<S> {
    
    
	int ACCESS_GRANTED = 1;
	int ACCESS_ABSTAIN = 0;
	int ACCESS_DENIED =1;
	
	boolean supports(ConfigAttribute var1);
	boolean supports(Class<?> var1);
	int vote(Authentication var1, S var2, Collection<ConfigAttribute> var3);
}

The return result of the vote() method will be one of the three constants defined in AccessDecisionVoter. ACCESS_GRANTED means agree, ACCESS_DENIED means deny, ACCESS_ABSTAIN means abstain. If an AccessDecisionVoter cannot determine whether the current Authentication has the right to access the corresponding protected object, the return value of its vote() method should be ACCESS_ABSTAIN.

Spring Security has three built-in voting-based AccessDecisionManager implementation classes as follows, which are AffirmativeBased , ConsensusBased , and UnanimousBased .

The logic of AffirmativeBased is:
(1) As long as there is an AccessDecisionVoter who votes ACCESS_GRANTED, the user is allowed to access;
(2) If all abstentions are also approved;
(3) If no one votes in favor, but someone votes against, the Throws AccessDeniedException.

Spring security uses AffirmativeBased by default.

The logic of ConsensusBased is:
(1) If there are more votes in favor than votes against, it means pass.
(2) Conversely, if there are more negative votes than positive votes, an AccessDeniedException will be thrown.
(3) If the approval votes are the same as the negative votes and are not equal to 0, and the value of the attribute allowIfEqualGrantedDeniedDecisions is true, it means that it
is passed, otherwise an exception AccessDeniedException will be thrown. The value of the parameter allowIfEqualGrantedDeniedDecisions defaults to true.
(4) If all AccessDecisionVoters have abstained, it will depend on the value of the parameter allowIfAllAbstainDecisions, if the value is true, it means pass, otherwise an exception AccessDeniedException will be thrown. The value of the parameter allowIfAllAbstainDecisions defaults to false.

The logic of UnanimousBased is a bit different from the other two implementations. The other two will pass all the configuration attributes of the protected object to AccessDecisionVoter for voting at one time, while UnanimousBased will only pass one ConfigAttribute to AccessDecisionVoter for voting at a time. This also means that if the logic of our AccessDecisionVoter is to vote in favor as long as one of the ConfigAttributes passed in can match, but the voting result in UnanimousBased is not necessarily in favor.

UnanimousBased的逻辑具体来说是这样的:
(1)如果受保护对象配置的某一个ConfigAttribute被任意的AccessDecisionVoter反对了,则将抛出AccessDeniedException。
(2)如果没有反对票,但是有赞成票,则表示通过。
(3)如果全部弃权了,则将视参数allowIfAllAbstainDecisions的值而定,true则通过,false则抛出AccessDeniedException。

Spring Security也内置一些投票者实现类如RoleVoter、AuthenticatedVoterWebExpressionVoter等,可以自行查阅资料进行学习。

4.3、自定义认证

Spring Security提供了非常好的认证扩展方法,比如:快速上手中将用户信息存储到内存中,实际开发中用户信息通常在数据库,Spring security可以实现从数据库读取用户信息,Spring security还支持多种授权方法。

4.3.1、自定义登录页面

在快速上手中,你可能会想知道登录页面从哪里来的?因为我们并没有提供任何的HTML或JSP文件。Spring Security的默认配置没有明确设定一个登录页面的URL,因此Spring Security会根据启用的功能自动生成一个登录页面URL,并使用默认URL处理登录的提交内容,登录后跳转的到默认URL等等。尽管自动生成的登录页面很方便快速启动和运行,但大多数应用程序都希望定义自己的登录页面。

4.3.1.1、认证页面

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="utf-8" %>
<html>
<head>
    <title>用户登录</title>
</head>
<body>
<form action="login" method="post">
    用户名:<input type="text" name="username"><br>&nbsp;&nbsp;&nbsp;码:
    <input type="password" name="password"><br>
    <input type="submit" value="登录">
</form>
</body>
</html>

4.3.1.2、配置认证页面

在WebConfig.java中配置认证页面地址:

//默认Url根路径跳转到/login,此url为spring security提供
@Override
public void addViewControllers(ViewControllerRegistry registry) {
    
    
	registry.addViewController("/").setViewName("redirect:/login‐view");
	registry.addViewController("/login‐view").setViewName("login");
}

4.3.1.3、安全配置

在WebSecurityConfig中配置表章登录信息:

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
                .and()
                .formLogin()//允许表单登录
                .loginPage("/login-view")//指定我们自己的登录页,spring security以重定向方式跳转到/login-view
                .loginProcessingUrl("/login")//指定登录处理的URL,也就是用户名、密码表单提交的目的路径
                .successForwardUrl("/login-success")//指定登录成功后的跳转URL
        		.permitAll();
                
    }
}

4.3.1.4、测试

Address: localbost:8000/security-springboot/login-view
When the user is not authenticated, accessing system resources will be redirected to the login-view page

In order to prevent CSRF (Cross-site request forgery) from happening, spring security restricts most methods except get.
Solution 1:
Shield CSRF control, that is, spring security no longer restricts CSRF.
Configure WebSecurityConfig

@Override
protected void configure(HttpSecurity http) throws Exception {
    
    
	http.csrf().disable() //屏蔽CSRF控制,即spring security不再限制CSRF
	...
}

Solution 2:
Add a token to the login.jsp page, and spring security will verify the token. If the token is legal, you can continue the request.
Modify login.jsp

<form action="login" method="post">
	<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
	...
</form>

4.3.2, Connection database authentication

In the previous example, we store user information in memory, and in actual projects, user information is stored in the database. This section realizes reading user information from the database. According to the previous research on the authentication process, it is only necessary to redefine the UserDetailService to query the database according to the user account.

4.3.2.1. Create database

Create user_db database

CREATE DATABASE `user_db` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';

Create t_user table

CREATE TABLE `t_user` (
	`id` bigint(20) NOT NULL COMMENT '用户id',
	`username` varchar(64) NOT NULL,
	`password` varchar(64) NOT NULL,
	`fullname` varchar(255) NOT NULL COMMENT '用户姓名',
	`mobile` varchar(11) DEFAULT NULL COMMENT '手机号',
	PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC

4.3.2.2, code implementation

1) Define dataSource
in application.properties configuration

spring.datasource.url=jdbc:mysql://localhost:3306/user_db
spring.datasource.username=root
spring.datasource.password=mysql
spring.datasource.driver‐class‐name=com.mysql.jdbc.Driver

2) Add dependencies

<dependency>
   <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
</dependency>

3) Define Dao

@Data
public class UserDto {
    
    
    private String id;
    private String username;
    private String password;
    private String fullname;
    private String mobile;
}

Define UserDao in the Dao package:

@Repository
public class UserDao {
    
    
	@Autowired
	JdbcTemplate jdbcTemplate;
	
	public UserDto getUserByUsername(String username){
    
    
		String sql ="select id,username,password,fullname from t_user where username = ?";
		List<UserDto> list = jdbcTemplate.query(sql, new Object[]{
    
    username}, new BeanPropertyRowMapper<>(UserDto.class));
		if(list == null && list.size() <= 0){
    
    
			return null;
		}
		return list.get(0);
	}
}

4.3.2.3. Define UserDetailService

Define SpringDataUserDetailsService under the service package:

@Service
public class SpringDataUserDetailsService implements UserDetailsService {
    
    
	@Autowired
	UserDao userDao;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
		//登录账号
		System.out.println("username="+username);
		//根据账号去数据库查询...
		UserDto user = userDao.getUserByUsername(username);
		if(user == null){
    
    
			return null;
		}
		//这里暂时使用静态数据
		UserDetails userDetails = User.withUsername(user.getFullname()).password(user.getPassword()).authorities("p1").build();
		return userDetails;
	}
}

4.3.2.4. Test

Enter the account number and password to request authentication and tracking code.

4.3.2.5. Use BCryptPasswordEncoder

According to the usage method of PasswordEncoder we mentioned earlier, using BCryptPasswordEncoder needs to complete the following tasks:
1. Define BCryptPasswordEncoder in the security configuration class

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

2. The password in UserDetails is stored in BCrypt format.
The user information is queried from the database, so the password in the database should be stored in BCrypt format.

4.4. Session

After the user is authenticated, the user's information can be saved in the session in order to avoid authentication for each operation of the user. Spring security provides session management. After the authentication is passed, the identity information is put into the SecurityContextHolder context, and the SecurityContext is bound to the current thread to facilitate the acquisition of user identity.

4.4.1. Obtain user identity

Write LoginController, realize the test resources of /r/r1 and /r/r2, and modify the loginSuccess method, pay attention to the getUsername method, the method for Spring Security to obtain the current login user information is SecurityContextHolder.getContext().getAuthentication()

@RestController
public class LoginController {
    
    
	/**
	* 用户登录成功
	* @return
	*/
	@RequestMapping(value = "/login‐success",produces = {
    
    "text/plain;charset=UTF‐8"})
	public String loginSuccess(){
    
    
		String username = getUsername()
		return username + " 登录成功";
	}

	/**
	* 获取当前登录用户名
	* @return
	*/
	private String getUsername(){
    
    
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if(!authentication.isAuthenticated()){
    
    
			return null;
		}
		Object principal = authentication.getPrincipal();
		String username = null;
		if (principal instanceof org.springframework.security.core.userdetails.UserDetails) {
    
    
			username = ((org.springframework.security.core.userdetails.UserDetails)principal).getUsername();
		} else {
    
    
			username = principal.toString();
		}
		return username;
	}
	
	/**
	* 测试资源1
	* @return
	*/
	@GetMapping(value = "/r/r1",produces = {
    
    "text/plain;charset=UTF‐8"})
	public String r1(){
    
    
		String username = getUsername();
		return username + " 访问资源1";
	}

	/**
	* 测试资源2
	* @return
	*/
	@GetMapping(value = "/r/r2",produces = {
    
    "text/plain;charset=UTF‐8"})
	public String r2(){
    
    
		String username = getUsername();
		return username + " 访问资源2";
	}
}

4.4.2, session control

We can control exactly when a session is created and how Spring Security interacts with it via the following options:

mechanism describe
always If no session exists, create one
ifRequired Create a Session if needed (default) when logging in
never Spring Security will not create a Session, but if a Session is created elsewhere in the application, Spring Security will use it.
stateless SpringSecurity will never create a Session, nor use a Session

Configure this option in the following configuration:

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    

	@Override
	protected void configure(HttpSecurity http) throws Exception {
    
    
		http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
	}
}

By default, Spring Security will create a new Session for each successful login user, which is ifRequired.

If you choose never, it instructs Spring Security not to create a Session for users who log in successfully, but if your application creates a new session somewhere, then Spring Security will use it.

If you use stateless, it means that Spring Security will not create a Session for users who log in successfully, and your application will not allow new sessions. And it will imply that no cookies are used, so every request needs to re-authenticate. This stateless architecture works well with REST APIs and its stateless authentication mechanism.

Session timeout
You can set the session timeout in the sevlet container, as follows to set the session validity period to 3600s;
spring boot configuration file:

server.servlet.session.timeout=3600s

After the session times out, you can set the jump path through Spring Security.

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    

	@Override
	protected void configure(HttpSecurity http) throws Exception {
    
    
		http.sessionManagement()
			.expiredUrl("/login‐view?error=EXPIRED_SESSION")
			.invalidSessionUrl("/login‐view?error=INVALID_SESSION");
	}
}

expired means that the session has expired, and invalidSession means that the incoming sessionid is invalid.

Secure session cookie
We can use httpOnly and secure tags to protect our session cookie:

  • httpOnly: If true, then browser scripts will not be able to access cookies

  • secure: if true, the cookie will only be sent over HTTPS connections

spring boot configuration file:

server.servlet.session.cookie.http‐only=true
server.servlet.session.cookie.secure=true

4.6. Exit

Spring security implements logout by default, access /logout, as expected, the exit function Spring has also done it for us.

Configure in protected void configure(HttpSecurity http) of WebSecurityConfig:

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
                .and()
                .formLogin()//允许表单登录
                .loginPage("/login-view")//指定我们自己的登录页,spring security以重定向方式跳转到/login-view
                .loginProcessingUrl("/login")//指定登录处理的URL,也就是用户名、密码表单提交的目的路径
                .successForwardUrl("/login-success")//指定登录成功后的跳转URL
                .and()
				.logout()
				.logoutUrl("/logout")
				.logoutSuccessUrl("/login‐view?logout");
                
    }
}

When the exit operation fires, this will happen:

  • Invalidate HTTP Session
  • Clear SecurityContextHolder
  • Jump to /login-view?logout

However, similar to configuring the login function, we can further customize the logout function:

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
                .and()
                .formLogin()//允许表单登录
                .loginPage("/login-view")//指定我们自己的登录页,spring security以重定向方式跳转到/login-view
                .loginProcessingUrl("/login")//指定登录处理的URL,也就是用户名、密码表单提交的目的路径
                .successForwardUrl("/login-success")//指定登录成功后的跳转URL
                .and()
				.logout()//提供系统退出支持,使用 WebSecurityConfigurerAdapter 会自动被应用
				.logoutUrl("/logout")//设置触发退出操作的URL (默认是 /logout )
				.logoutSuccessUrl("/login‐view?logout")//退出之后跳转的URL。默认是 /login?logout 
				.logoutSuccessHandler(logoutSuccessHandler) //定制的 LogoutSuccessHandler ,用于实现用户退出成功时的处理。如果指定了这个选项那么logoutSuccessUrl() 的设置会被忽略。
				.addLogoutHandler(logoutHandler) //添加一个 LogoutHandler ,用于实现用户退出时的清理工作.默认 SecurityContextLogoutHandler 会被添加为最后一个 LogoutHandler 
				.invalidateHttpSession(true);//指定是否在退出时让 HttpSession 无效。 默认设置为 true。
                
    }
}

Note: If you want logout to take effect under the GET request, you must close csrf().disable() to prevent CSRF attacks. If CSRF is enabled, you must use the post method to request /logou

logoutHandler :
In general, LogoutHandler implementations are used to perform necessary cleanup, so they should not throw exceptions.

Here are some implementations provided by Spring Security:

  • PersistentTokenBasedRememberMeServices Cleanup related to the RememberMe function based on the persistent token

  • TokenBasedRememberMeService Cleanup related to token-based RememberMe function

  • Cookie related cleaning when CookieClearingLogoutHandler exits

  • CsrfLogoutHandler is responsible for removing csrfToken when exiting

  • SecurityContext related cleanup when SecurityContextLogoutHandler exits

The chain API provides a shortcut to call the corresponding LogoutHandler implementation, such as deleteCookies().

4.7. Authorization

4.7.1. Overview

Authorization methods include web authorization and method authorization

  • Web authorization is authorized through url interception
  • Method authorization is authorized through method interception
  • They will all call accessDecisionManager for authorization decisions
  • If it is web authorization, the interceptor is FilterSecurityInterceptor;
  • If it is method authorization, the interceptor is MethodSecurityInterceptor.
  • If the web authorization and method authorization are passed at the same time, the web authorization is executed first, and then the method authorization is executed. Finally, if the decision is passed, access to the resource is allowed, otherwise access is prohibited.

The class relationships are as follows:

insert image description here

4.7.2. Prepare the environment

4.7.2.1, database environment

Create the following table in the t_user database:

Role table:

CREATE TABLE `t_role` (
	`id` varchar(32) NOT NULL,
	`role_name` varchar(255) DEFAULT NULL,
	`description` varchar(255) DEFAULT NULL,
	`create_time` datetime DEFAULT NULL,
	`update_time` datetime DEFAULT NULL,
	`status` char(1) NOT NULL,
	PRIMARY KEY (`id`),
	UNIQUE KEY `unique_role_name` (`role_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

insert into `t_role`(`id`,`role_name`,`description`,`create_time`,`update_time`,`status`) values('1','管理员',NULL,NULL,NULL,'');

User role relationship table:

CREATE TABLE `t_user_role` (
	`user_id` varchar(32) NOT NULL,
	`role_id` varchar(32) NOT NULL,
	`create_time` datetime DEFAULT NULL,
	`creator` varchar(255) DEFAULT NULL,
	PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

insert into `t_user_role`(`user_id`,`role_id`,`create_time`,`creator`) values('1','1',NULL,NULL);

Permissions table:

CREATE TABLE `t_permission` (
	`id` varchar(32) NOT NULL,
	`code` varchar(32) NOT NULL COMMENT '权限标识符',
	`description` varchar(64) DEFAULT NULL COMMENT '描述',
	`url` varchar(128) DEFAULT NULL COMMENT '请求地址',
	PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

insert into `t_permission`(`id`,`code`,`description`,`url`) values ('1','p1','测试资源1','/r/r1'),('2','p3','测试资源2','/r/r2');

Role permission relationship table:

CREATE TABLE `t_role_permission` (
	`role_id` varchar(32) NOT NULL,
	`permission_id` varchar(32) NOT NULL,
	PRIMARY KEY (`role_id`,`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

insert into `t_role_permission`(`role_id`,`permission_id`) values ('1','1'),('1','2');

4.7.2.2. Modify UserDetailService

1. Modify the dao interface
Add in UserDao:

//根据用户id查询用户权限
public List<String> findPermissionsByUserId(String userId){
    
    
    String sql = "SELECT * FROM t_permission WHERE id IN(\n" +
            "\n" +
            "SELECT permission_id FROM t_role_permission WHERE role_id IN(\n" +
            "  SELECT role_id FROM t_user_role WHERE user_id = ? \n" +
            ")\n" +
            ")\n";

    List<PermissionDto> list = jdbcTemplate.query(sql, new Object[]{
    
    userId}, new BeanPropertyRowMapper<>(PermissionDto.class));
    List<String> permissions = new ArrayList<>();
    list.forEach(c -> permissions.add(c.getCode()));
    return permissions;
}

2. Modify UserDetailService
to achieve read permission from the database

//根据 账号查询用户信息
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    

    //将来连接数据库根据账号查询用户信息
    UserDto userDto = userDao.getUserByUsername(username);
    if(userDto == null){
    
    
        //如果用户查不到,返回null,由provider来抛出异常
        return null;
    }
    //根据用户的id查询用户的权限
    List<String> permissions = userDao.findPermissionsByUserId(userDto.getId());
    //将permissions转成数组
    String[] permissionArray = new String[permissions.size()];
    permissions.toArray(permissionArray);
    UserDetails userDetails = User.withUsername(userDto.getUsername()).password(userDto.getPassword()).authorities(permissionArray).build();
    return userDetails;
}

4.7.3, web authorization

In the above example, we have completed authentication interception and simple authorization protection for some resources under /r/**, but what should we do if we want to implement flexible authorization control? Customize requirements to our URL by adding multiple child nodes to http.authorizeRequests(), as follows:

@Override
protected void configure(HttpSecurity http) throws Exception {
    
    
	http
		.authorizeRequests() //http.authorizeRequests() 方法有多个子节点,每个macher按照他们的声明顺序执行。
		.antMatchers("/r/r1").hasAuthority("p1")//指定"/r/r1"URL,拥有p1权限能够访问
		.antMatchers("/r/r2").hasAuthority("p2") //指定"/r/r2"URL,拥有p2权限能够访问
		.antMatchers("/r/r3").access("hasAuthority('p1') and hasAuthority('p2')")//指定了"/r/r3"URL,同时拥有p1和p2权限才能够访问
		.antMatchers("/r/**").authenticated() //指定了除了r1、r2、r3之外"/r/**"资源,同时通过身份认证就能够访问,这里使用SpEL(Spring Expression Language)表达式
		.anyRequest().permitAll() //剩余的尚未匹配的资源,不做保护。
		.and()
		.formLogin()
	// ...
}

Notice:

The order of the rules is important, more specific rules should be written first.
Now everything starting with /admin requires an authenticated user with the ADMIN role, even the /admin/login path (since /admin/login is already replaced by /admin /** rule matches, so the second rule is ignored).

.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/admin/login").permitAll()

So the rules for the login page should come before the /admin/** rules. E.g.

.antMatchers("/admin/login").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")

Common methods for protecting URLs are:

  • authenticated() protects the URL and requires the user to be logged in

  • permitAll() The specified URL does not need to be protected, general applications and static resource files

  • hasRole(String role) restricts access to a single role, the role will be incremented by "ROLE_". So "ADMIN" will be compared with "ROLE_ADMIN".

  • hasAuthority(String authority) restricts access to a single authority

  • hasAnyRole(String... roles) Allows multiple roles to access.

  • hasAnyAuthority(String… authorities) Allow multiple authority access.

  • access(String attribute) This method uses SpEL expressions, so complex restrictions can be created.

  • hasIpAddress(String ipaddressExpression) restrict IP address or subnet

4.7.4, method authorization

Now we have mastered how to use http.authorizeRequests() to authorize and protect web resources. Starting from Spring Security2.0, it supports the security support of service layer methods. This section learns @PreAuthorize, @PostAuthorize, @Secured three types of annotations.

We can use @EnableGlobalMethodSecurity annotation on any @Configuration instance to enable annotation based security.

The following will enable Spring Security's @Secured annotation.

@EnableGlobalMethodSecurity(securedEnabled = true)
public class MethodSecurityConfig {
    
    // ...}

Adding an annotation to a method (on a class or interface) then restricts access to that method. Spring Security's native annotation support defines a set of attributes for this method. These will be passed to the AccessDecisionManager for it to make the actual decision:

public interface BankService {
    
    
	@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
	public Account readAccount(Long id);
	
	@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
	public Account[] findAccounts();
	
	@Secured("ROLE_TELLER")
	public Account post(Account account, double amount);
}

The above configuration indicates that the readAccount and findAccounts methods can be accessed anonymously, and the bottom layer uses the WebExpressionVoter voter, which can be traced from the 23rd line of AffirmativeBased. .

The post method requires a TELLER role to access, and the bottom layer uses the RoleVoter voter.

Use the following code to enable support for prePost annotations

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
    
    
	// ...
}

The corresponding Java code is as follows:

public interface BankService {
    
    
	@PreAuthorize("isAnonymous()")
	public Account readAccount(Long id);
	
	@PreAuthorize("isAnonymous()")
	public Account[] findAccounts();
	
	@PreAuthorize("hasAuthority('p_transfer') and hasAuthority('p_read_account')")
	public Account post(Account account, double amount);
}

The above configuration indicates that the readAccount and findAccounts methods can be accessed anonymously. The post method needs to have both p_transfer and p_read_account permissions to access. The bottom layer uses the WebExpressionVoter voter, which can be traced from the 23rd line of AffirmativeBased code.

5. Distributed System Authentication Scheme

5.1. What is a distributed system

As the software environment and requirements change, the software architecture evolves from a single structure to a distributed architecture. A system with a distributed architecture is called a distributed system. The operation of a distributed system usually relies on the network. For several services, the services interact with each other through the network to complete the user's business processing. The current popular microservice architecture is a distributed system architecture, as shown in the following figure:

insert image description here

The basic characteristics of a distributed system are as follows:

  • 1. Distributed: Each part can be deployed independently, and the interaction between services communicates through the network, such as: order service, commodity service.

  • 2. Scalability: Each part can be deployed in cluster mode, and hardware and software expansion can be carried out for some nodes, with certain scalability.

  • 3. Shareability: Each part can provide external services as shared resources, and multiple parts may operate shared resources.

  • 4. Openness: Each part can publish the access interface of shared resources according to the needs, and can allow third-party systems to access.

5.2. Distributed Authentication Requirements

Each service in a distributed system will have authentication and authorization requirements. If each service implements a set of authentication and authorization logic, it will be very redundant. Considering the sharing characteristics of distributed systems, an independent authentication service needs to handle system authentication and authorization. request; considering the characteristics of the openness of the distributed system, it not only provides authentication for internal services of the system, but also provides authentication for third-party systems. The requirements for distributed authentication are summarized as follows:

Unified authentication and authorization

Provide independent authentication services and handle authentication and authorization in a unified manner.

Regardless of different types of users or different types of clients (web, H5, APP), they all adopt consistent authentication, authority, and session mechanisms to achieve unified authentication and authorization.

To achieve unification, the authentication method must be scalable and support various authentication requirements, such as: user name password authentication, SMS verification code, QR code, face recognition and other authentication methods, and can be switched very flexibly.

Application Access Authentication

It should provide expansion and opening capabilities, provide a secure system docking mechanism, and open some APIs for third-party access. One-party applications (internal system services) and third-party applications (third-party applications) all use a unified mechanism for access.

5.3. Distributed authentication scheme

5.3.1. Type selection analysis

1. Session-based authentication method
In a distributed environment, there will be a problem with session-based authentication. Each application service needs to store user identity information in the session, and distribute local requests to another application service through load balancing The session information needs to be brought over, otherwise it will be re-authenticated.

insert image description here

At this time, the usual methods are as follows:

  • Session replication: Synchronize sessions between multiple application servers to keep sessions consistent and transparent to the outside world.

  • Session sticking: When a user accesses a server in the cluster, it is mandatory to specify that all subsequent requests fall on this machine.

  • Centralized session storage: store the session in the distributed cache, and all server application instances access the session from the distributed cache uniformly.

Generally speaking, the authentication method based on session authentication can better control the session on the server side, and has higher security. However, the session mechanism is based on cookies, which cannot be used effectively on complex and diverse mobile clients, and cannot cross domains. In addition, with the expansion of the system, the fault tolerance of session copying, pasting and
storage needs to be improved.

2. Token-based authentication method
With the token-based authentication method, the server does not need to store authentication data, which is easy to maintain and has strong scalability. The client can store the token anywhere, and can realize the unified authentication mechanism of web and app. Its disadvantages are also obvious. Due to its self-contained information, the token generally has a large amount of data, and it needs to be transmitted every time it is requested, so it occupies a lot of bandwidth. In addition, the token signature verification operation will also bring additional processing load to the CPU.

insert image description here

5.3.2. Technical solution

According to the analysis of the selection, it is decided to adopt the token-based authentication method. Its advantages are:

  • 1. A mechanism suitable for unified authentication. Clients, one-party applications, and three-party applications all follow a consistent authentication mechanism.

  • 2. The token authentication method is more suitable for third-party application access because it is more open and can use the currently popular open protocols Oauth2.0, JWT, etc.

  • 3. In general, the server does not need to store session information, which reduces the pressure on the server.

The distributed system authentication technical scheme is shown in the figure below:insert image description here

Process description:

  • (1) The user logs in through the access party (application), and the access party adopts OAuth2.0 to authenticate in the unified authentication service (UAA).

  • (2) The authentication service (UAA) calls to verify whether the user's identity is legal, and obtain user permission information.

  • (3) The authentication service (UAA) obtains the permission information of the access party, and verifies whether the access party is legal.

  • (4) If both the logged-in user and the access party are legal, the authentication service generates a jwt token and returns it to the access party, where the jwt includes user permissions and access party permissions.

  • (5) Subsequently, the access party carries the jwt token to access the microservice resources in the API gateway.

  • (6) The API gateway parses the token and verifies whether the authority of the access party can access the requested microservice.

  • (7) If the permissions of the accessing party are okay, the API gateway will attach the parsed plaintext Token to the header of the original request, and forward the request to the microservice.

  • (8) The microservice receives the request, and the plaintext token contains the identity and permission information of the logged-in user. Therefore, subsequent microservices can do two things by themselves:

    • 1: User authorization interception (see if the current user has the right to access the resource)
    • 2: Store user information into the current thread context (beneficial for subsequent business logic to obtain current user information at any time)

The responsibilities of UAA services and API gateway components involved in the process are as follows:

1) Unified Authentication Service (UAA),
which bears the responsibilities of OAuth2.0 access party authentication, login user authentication, authorization, and token generation, and completes the actual user authentication and authorization functions.

2) API Gateway
As the only entrance to the system, the API Gateway provides a customized API collection for the access party, and it may also have other responsibilities, such as authentication, monitoring, load balancing, caching, etc. The core point of the API gateway method is that all access parties and consumers access microservices through a unified gateway, and handle all non-business functions at the gateway layer.

Guess you like

Origin blog.csdn.net/shuai_h/article/details/130653929