Authentication and Authorization¶
Generally speaking, application access security revolves around the two core concepts of authentication and authorization.
Right now:
- First you need to identify the user identity,
- Then determine whether the user has access to the specified resources.
There are many solutions for authentication, the mainstream ones are CAS
, , SAML2
, OAUTH2
etc. (unfortunately, these have been used -_-). The single sign-on solution (SSO) we often talk about is this.
For authorization, the mainstream ones are spring security and shiro.
Shiro is relatively lightweight. In comparison, spring security does have a more complex architecture. But shiro and ss, just master one.
Here, Nien will give you a systematic and systematic sorting out, for the reference of the small partners in the future, and improve everyone's 3-level architecture, design, and development level.
Note: This article is continuously updated in PDF. For the latest PDF files of Nien’s architecture notes and interview questions, please get them from the link below: Code Cloud
What is OAuth2?
OAuth2 is an open standard about authorization . The core idea is to authenticate user identities through various authentication methods (OAuth2 does not care about the specific methods).
And issue a token (token), so that third-party applications can use the token to access specified resources within a limited time and within a limited scope .
The main RFC specifications involved are [ RFC6749
(overall authorization framework)], [ RFC6750
(token usage)], and [ RFC6819
(threat model)], which are generally what we need to understand RFC6749
.
There are four main ways to obtain tokens, namely 授权码模式
,简单模式
, 密码模式
, 客户端模式
,
In short: OAuth2 is an authorization (Authorization) protocol.
Authentication (Authentication) proves that you are this person,
And authorization (Authorization) is to prove whether this person has access to this resource (Resource).
The following picture comes from the OAuth 2.0 authorization framework RFC Document , which is an abstract process of OAuth2.
+--------+ +---------------+
| |--(A)- Authorization Request ->| Resource |
| | | Owner |
| |<-(B)-- Authorization Grant ---| |
| | +---------------+
| |
| | +---------------+
| |--(C)-- Authorization Grant -->| Authorization |
| Client | | Server |
| |<-(D)----- Access Token -------| |
| | +---------------+
| |
| | +---------------+
| |--(E)----- Access Token ------>| Resource |
| | | Server |
| |<-(F)--- Protected Resource ---| |
+--------+ +---------------+
Let me first explain the nouns in the above picture:
Resource Owner : the resource owner, that is, the user
Client : client application (Application)
Authorization Server : authorization server
Resource Server : resource server
Let's explain the general process of the above figure:
(A) After the user connects to the client application, the client application asks the user for authorization
(B) The user agrees to authorize the client application
(C) The client application uses the authorization (Grant) obtained in the previous step to apply for a token from the authorization server
(D) After the authorization server verifies the authorization (Grant) of the client application, it confirms that it is correct and issues a token
(E) The client application uses the token to apply to the resource server for resources
(F) The resource server confirms that the token is correct and agrees to open resources to the client application
As can be seen from the above process, how to obtain the **authorization (Grant)** is the key.
In OAuth2 there are 4 grant types:
- Authorization Code (authorization code mode) :
The authorization model with the most complete functions and the strictest process. Interact with the authentication server through a third-party application server. Widely used in various third-party authentication.
- Implicit (simplified mode):
Apply for a token directly to the authentication server in the browser without going through a third-party application server, which is more suitable for mobile apps and third-party single-page applications without a server.
- Resource Owner Password (password mode) :
Users provide their own user names and passwords to the client server, which are used when users have a high degree of trust in the client, such as internal systems of companies and organizations, SSO.
- Client Credentials (client mode):
The client server authenticates to the authentication server on its own behalf, not on the user's behalf.
The following mainly talks about the most commonly used (1) and (3). In addition, there is another mode called Refresh Token, which will be introduced below.
Resource Owner Password (password mode)
+----------+
| Resource |
| Owner |
| |
+----------+
v
| Resource Owner
(A) Password Credentials
|
v
+---------+ +---------------+
| |>--(B)---- Resource Owner ------->| |
| | Password Credentials | Authorization |
| Client | | Server |
| |<--(C)---- Access Token ---------<| |
| | (w/ Optional Refresh Token) | |
+---------+ +---------------+
Figure 5: Resource Owner Password Credentials Flow
Its steps are as follows:
(A) The user (Resource Owner) provides the user name and password to the client (Client).
(B) The client sends the user name and password to the authentication server (Authorization Server), and requests a token from the latter.
(C) After the authentication server confirms that it is correct, it provides the access token to the client.
Authorization Code (authorization code mode)
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| -+----(A)-- & Redirection URI ---->| |
| User- | | Authorization |
| Agent -+----(B)-- User authenticates --->| Server |
| | | |
| -+----(C)-- Authorization Code ---<| |
+-|----|---+ +---------------+
| | ^ v
(A) (C) | |
| | | |
^ v | |
+---------+ | |
| |>---(D)-- Authorization Code ---------' |
| Client | & Redirection URI |
| | |
| |<---(E)----- Access Token -------------------'
+---------+ (w/ Optional Refresh Token)
Note: The lines illustrating steps (A), (B), and (C) are broken into
two parts as they pass through the user-agent.
Its steps are as follows:
(A) The user (Resource Owner) accesses the client (Client) through the user agent (User-Agent), and the client asks for authorization and directs the user to the authentication server (Authorization Server).
(B) The user chooses whether to authorize the client.
(C) Assuming that the user grants authorization, the authentication server directs the user to the "redirection URI" (redirection URI) specified by the client in advance, and attaches an authorization code at the same time.
(D) The client receives the authorization code, attaches the previous "redirect URI", and applies for a token to the authentication server. This step is done on the server in the background of the client and is not visible to the user.
(E) The authentication server checks the authorization code and the redirection URI, and after confirming that it is correct, sends the access token (access token) and update token (refresh token) to the client. This step is also invisible to the user.
Token refresh (refresh token)
+--------+ +---------------+
| |--(A)------- Authorization Grant --------->| |
| | | |
| |<-(B)----------- Access Token -------------| |
| | & Refresh Token | |
| | | |
| | +----------+ | |
| |--(C)---- Access Token ---->| | | |
| | | | | |
| |<-(D)- Protected Resource --| Resource | | Authorization |
| Client | | Server | | Server |
| |--(E)---- Access Token ---->| | | |
| | | | | |
| |<-(F)- Invalid Token Error -| | | |
| | +----------+ | |
| | | |
| |--(G)----------- Refresh Token ----------->| |
| | | |
| |<-(H)----------- Access Token -------------| |
+--------+ & Optional Refresh Token +---------------+
When we applied for token, Authorization Server not only gave us Access Token, but also Refresh Token.
When the Access Token expires, we can use the Refresh Token to access the /refresh endpoint to get a new Access Token.
We have to distinguish it from Spring Security's Authentication ,
What is Spring Security?
Spring Security is a security framework that can control user access rights based on RBAC (role-based access control),
The core idea is to intercept and filter through a series of filter chains to control the user's access rights.
The core functions of spring security mainly include:
- Authentication (who you are)
- authorization (what can you do)
- Attack protection (prevention of forged identities)
At its core is a set of filter chains that will be automatically configured when the project starts. The core is that the Basic Authentication Filter is used to authenticate the user's identity, and a filter in spring security handles an authentication method.
For example, for the username password authentication filter,
Will check if it is a login request;
Whether to include username and password (that is, some authentication information required by the filter);
If not satisfied, release to the next one.
The next step is to judge whether it is the information you need according to your own responsibilities. The characteristic of basic is that there is Authorization: Basic eHh4Onh4 information in the request header. There may be more authentication filters in between. The last link is FilterSecurityInterceptor , which will determine whether the request can access the rest service. The basis for the judgment is the configuration in BrowserSecurityConfig. If it is rejected, different exceptions will be thrown (according to the specific reason). Exception Translation Filter will catch the thrown error, and then return information according to different authentication methods.
Note: The green filter can be configured to take effect, and the others cannot be controlled.
2. Getting started project
First create the spring boot project HelloSecurity, whose pom mainly depends on the following:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Then create a page in the src/main/resources/templates/ directory:
home.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p>
</body>
</html>
As we can see, a link is included in this simple view: "/hello". Linking to the following page, the Thymeleaf template is as follows:
hello.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>
The web application is based on Spring MVC. Therefore, you need to configure Spring MVC and set up view controllers to expose these templates. The following is a typical Spring MVC configuration class. In the src/main/java/hello directory (so java is here):
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
}
The addViewControllers() method (overriding the WebMvcConfigurerAdapter method of the same name) adds four view controllers. Two view controllers refer to a view named "home" (defined in home.html), and the other refers to a view named "hello" (defined in hello.html). The fourth view controller references another view called "login". This view will be created in the next section. At this point, you can skip ahead to make the application executable and run the application without logging into anything. Then start the program as follows:
@SpringBootApplication
public class Application {
public static void main(String[] args) throws Throwable {
SpringApplication.run(Application.class, args);
}
}
2. Join Spring Security
Suppose you want to prevent unauthorized users from accessing "/hello". At this point, if the user clicks on the link on the home page, they see the greeting and the request is not blocked. You need to add a barrier to make the user log in before seeing the page. You can do this by configuring Spring Security in your application. If Spring Security is on the classpath, Spring Boot automatically secures all HTTP endpoints using "Basic Authentication". At the same time, you can further customize the security settings. First introduce in the pom file:
<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
...
</dependencies>
The following is the security configuration so that only authenticated users can access the greeting page:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
}
The WebSecurityConfig class uses the @EnableWebSecurity annotation to enable Spring Security's web security support and provide Spring MVC integration. It also extends WebSecurityConfigurerAdapter and overrides some methods to set some details of the web security configuration.
The configure(HttpSecurity) method defines which URL paths should be secured and which should not. Specifically, the "/" and "/home" paths are configured not to require any authentication. All other paths must be authenticated.
When a user successfully logs in, they will be redirected to the previously requested page that requires authentication. There is a custom "/login" page specified by loginPage() that everyone can view.
For the configureGlobal(AuthenticationManagerBuilder) method, it sets a single user in memory. The user name of this user is "user", the password is "password", and the role is "USER".
Now we need to create the login page. We already configured the "login" view controller earlier, so now we only need to create the login page:
login.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<form th:action="@{/login}" method="post">
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password: <input type="password" name="password"/> </label></div>
<div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>
As you can see, this Thymeleaf template just provides a form to get username and password and submit them to "/login". Depending on configuration, Spring Security provides a filter that intercepts this request and authenticates the user. If the user is not authenticated, the page will redirect to "/login?error" and display the appropriate error message on the page. After a successful logout, our application sends to "/login?logout" and our page displays a corresponding logout success message. Finally, we need to provide the user with a method to display the current username and logout. Update hello.html to print a hello to the current user and include a "logout" form as follows:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out"/>
</form>
</body>
</html>
3. Detailed explanation of parameters
1. Annotate @EnableWebSecurity
Using Spring Security in Spring boot applications, the @EnableWebSecurity annotation is used. The official description is that this annotation is used together with the @Configuration annotation to annotate the class of type WebSecurityConfigurer, or use the @EnableWebSecurity annotation to inherit the class of WebSecurityConfigurerAdapter, thus forming Spring Security Configuration.
2. Abstract class WebSecurityConfigurerAdapter
In general, you will choose to inherit the WebSecurityConfigurerAdapter class. The official description is: WebSecurityConfigurerAdapter provides a convenient way to create an instance of WebSecurityConfigurer. You only need to rewrite the method of WebSecurityConfigurerAdapter to configure what URLs to intercept, what permissions to set, and other security controls.
3、方法 configure(AuthenticationManagerBuilder auth) 和 configure(HttpSecurity http)
Two methods of WebSecurityConfigurerAdapter are rewritten in Demo:
/**
* 通过 {@link #authenticationManager()} 方法的默认实现尝试获取一个 {@link AuthenticationManager}.
* 如果被复写, 应该使用{@link AuthenticationManagerBuilder} 来指定 {@link AuthenticationManager}.
*
* 例如, 可以使用以下配置在内存中进行注册公开内存的身份验证{@link UserDetailsService}:
*
* // 在内存中添加 user 和 admin 用户
* @Override
* protected void configure(AuthenticationManagerBuilder auth) {
* auth
* .inMemoryAuthentication().withUser("user").password("password").roles("USER").and()
* .withUser("admin").password("password").roles("USER", "ADMIN");
* }
*
* // 将 UserDetailsService 显示为 Bean
* @Bean
* @Override
* public UserDetailsService userDetailsServiceBean() throws Exception {
* return super.userDetailsServiceBean();
* }
*
*/
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
this.disableLocalConfigureAuthenticationBldr = true;
}
/**
* 复写这个方法来配置 {@link HttpSecurity}.
* 通常,子类不能通过调用 super 来调用此方法,因为它可能会覆盖其配置。 默认配置为:
*
* http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
*
*/
protected void configure(HttpSecurity http) throws Exception {
logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
4. Final class HttpSecurity
HttpSecurity common methods and instructions:
method | illustrate |
---|---|
openidLogin() |
For OpenId based authentication |
headers() |
Add a security header to the response |
cors() |
Configure Cross-Origin Resource Sharing ( CORS ) |
sessionManagement() |
Allows configuration of session management |
portMapper() |
Allows to configure one PortMapper ( HttpSecurity#(getSharedObject(class)) ), other provided SecurityConfigurer objects use redirection PortMapper from HTTP to HTTPS or from HTTPS to HTTP. By default, Spring Security uses a PortMapperImpl mapping HTTP port 8080 to HTTPS port 8443, HTTP port 80 to HTTPS port 443 |
jee() |
Configure container-based pre-authentication. In this case authentication is managed by the Servlet container |
x509() |
Configure x509-based authentication |
rememberMe |
Allows configuration of "remember me" authentication |
authorizeRequests() |
HttpServletRequest Allow access based on usage restrictions |
requestCache() |
Allows configuring request caching |
exceptionHandling() |
Allow configuration error handling |
securityContext() |
Management set on HttpServletRequests between . This will be automatically applied when usingSecurityContextHolder SecurityContext WebSecurityConfigurerAdapter |
servletApi() |
Integrate HttpServletRequest the method with the value found on it SecurityContext . WebSecurityConfigurerAdapter This will be automatically applied when using |
csrf() |
Add CSRF support, WebSecurityConfigurerAdapter when used, enabled by default |
logout() |
Added logout support. When used WebSecurityConfigurerAdapter , this will be applied automatically. By default, access the URL "/logout", invalidate the HTTP Session to clear the user, clear any #rememberMe() authentication that has been configured, clear SecurityContextHolder , then redirect to "/login?success" |
anonymous() |
Allows configuration of the representation of anonymous users. WebSecurityConfigurerAdapter This will be applied automatically when used in conjunction with By default anonymous users will be org.springframework.security.authentication.AnonymousAuthenticationToken represented using and include the role "ROLE_ANONYMOUS" |
formLogin() |
Specifies that forms-based authentication is supported. If not specified FormLoginConfigurer#loginPage(String) , a default login page will be generated |
oauth2Login() |
Configure authentication against an external OAuth 2.0 or OpenID Connect 1.0 provider |
requiresChannel() |
Configure channel security. For this configuration to be useful, at least one mapping to the desired channel must be provided |
httpBasic() |
Configure Http Basic Authentication |
addFilterAt() |
Add a filter at the location of the specified Filter class |
5、类 AuthenticationManagerBuilder
/**
* {@link SecurityBuilder} used to create an {@link AuthenticationManager}. Allows for
* easily building in memory authentication, LDAP authentication, JDBC based
* authentication, adding {@link UserDetailsService}, and adding
* {@link AuthenticationProvider}'s.
*/
It means that AuthenticationManagerBuilder is used to create an AuthenticationManager, which allows me to easily implement memory authentication, LADP authentication, JDBC-based authentication, add UserDetailsService, and add AuthenticationProvider.
Use the username and password defined in the yaml file to log in
Define username and password in application.yaml:
spring:
security:
user:
name: root
password: root
Log in with root/root, and you can access normally /hello
.
Log in with the username and password specified in the code
- Use configure(AuthenticationManagerBuilder) to add authentication.
- Use configure(httpSecurity) to add permissions
@Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("admin") // 添加用户admin
.password("{noop}admin") // 不设置密码加密
.roles("ADMIN", "USER")// 添加角色为admin,user
.and()
.withUser("user") // 添加用户user
.password("{noop}user")
.roles("USER")
.and()
.withUser("tmp") // 添加用户tmp
.password("{noop}tmp")
.roles(); // 没有角色
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/product/**").hasRole("USER") //添加/product/** 下的所有请求只能由user角色才能访问
.antMatchers("/admin/**").hasRole("ADMIN") //添加/admin/** 下的所有请求只能由admin角色才能访问
.anyRequest().authenticated() // 没有定义的请求,所有的角色都可以访问(tmp也可以)。
.and()
.formLogin().and()
.httpBasic();
}
}
添加AdminController、ProductController
@RestController
@RequestMapping("/admin")
public class AdminController {
@RequestMapping("/hello")
public String hello(){
return "admin hello";
}
}
@RestController
@RequestMapping("/product")
public class ProductController {
@RequestMapping("/hello")
public String hello(){
return "product hello";
}
}
Through the above settings, access to http://localhost:8080/admin/hello can only be accessed by admin, http://localhost:8080/product/hello can be accessed by both admin and user, http://localhost:8080/hello All users (including tmp) have access.
Login with database user name and password
add dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
Add database configuration
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
Configure spring-security authentication and authorization
@Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)// 设置自定义的userDetailsService
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/product/**").hasRole("USER")
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated() //
.and()
.formLogin()
.and()
.httpBasic()
.and().logout().logoutUrl("/logout");
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();// 使用不使用加密算法保持密码
// return new BCryptPasswordEncoder();
}
}
If you need to use it BCryptPasswordEncoder
, you can first encrypt it in the test environment and put it in the database:
@Test
void encode() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String password = bCryptPasswordEncoder.encode("user");
String password2 = bCryptPasswordEncoder.encode("admin");
System.out.println(password);
System.out.println(password2);
}
Configure a custom UserDetailsService for authentication
@Component("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
// 1. 查询用户
User userFromDatabase = userRepository.findOneByLogin(login);
if (userFromDatabase == null) {
//log.warn("User: {} not found", login);
throw new UsernameNotFoundException("User " + login + " was not found in db");
//这里找不到必须抛异常
}
// 2. 设置角色
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(userFromDatabase.getRole());
grantedAuthorities.add(grantedAuthority);
return new org.springframework.security.core.userdetails.User(login,
userFromDatabase.getPassword(), grantedAuthorities);
}
}
Configure UserRepository in JPA
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
User findOneByLogin(String login);
}
Add database data
CREATE TABLE `user` (
`id` int(28) NOT NULL,
`login` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`password` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`role` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT INTO `demo`.`user`(`id`, `login`, `password`, `role`) VALUES (1, 'user', 'user', 'ROLE_USER');
INSERT INTO `demo`.`user`(`id`, `login`, `password`, `role`) VALUES (2, 'admin', 'admin', 'ROLE_ADMIN');
The default role prefix must be
ROLE_
, because spring-security will automatically use the role in match for comparison during authorizationROLE_
.
Four: Obtain login information
@RequestMapping("/info")
public String info(){
String userDetails = null;
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if(principal instanceof UserDetails) {
userDetails = ((UserDetails)principal).getUsername();
}else {
userDetails = principal.toString();
}
return userDetails;
}
Use SecurityContextHolder.getContext().getAuthentication().getPrincipal();
to get the current login information.
Five: Spring Security core components
SecurityContext
SecurityContext
It is a security context, and all data is saved in SecurityContext.
SecurityContext
Objects that can be obtained through :
- Authentication
SecurityContextHolder
SecurityContextHolder
Utilities used to retrieve data held in the SecurityContext. Obtain the corresponding data of the SecurityContext by using the static method.
SecurityContext context = SecurityContextHolder.getContext();
Authentication
Authentication indicates the current authentication status, and the objects that can be obtained are:
UserDetails: Obtain additional information such as user information, whether to lock or not.
Credentials: Get the password.
isAuthenticated: Gets whether it has been authenticated.
Principal: Get the user, if not authenticated, then it is the username, if authenticated, return UserDetails.
UserDetails
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
UserDetailsService
UserDetailsService can get UserDetails object through loadUserByUsername. This interface is used by spring security for user authentication.
Usually, a custom UserDetailsService is used to implement the UserDetailsService interface, and UserDetails is queried through customization.
AuthenticationManager
AuthenticationManager is used for verification, and if the verification fails, a corresponding exception will be thrown.
PasswordEncoder
password encryptor. Usually custom specified.
BCryptPasswordEncoder: hash algorithm encryption
NoOpPasswordEncoder: no encryption is used
Six: spring security session stateless support permission control (separation before and after)
Spring security will put authentication information into HttpSession by default.
But for the situation where our front and back ends are separated, such as apps, small programs, web front and back separation, etc., httpSession is useless. At this time, we can
configure(httpSecurity)
set whether to use httpSession in spring security.
@Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
// code...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
//设置无状态,所有的值如下所示。
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// code...
}
// code...
}
There are four values, of which the default is ifRequired.
- always – a session will always be created if one doesn’t already exist,没有session就创建。
- ifRequired – a session will be created only if required ( default ), if required (default).
- never – the framework will never create a session itself but it will use one if it already exists
- stateless – no session will be created or used by Spring Security
Since the front and back ends do not judge by saving sessions and cookies, in order to ensure that spring security can record the login status, it is necessary to pass a value so that the value can self-verify the source and obtain data information at the same time. For type selection, we choose JWT . For the java client we choose to use jjwt .
add dependencies
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
Create a tool class JWTProvider
JWTProvider needs to provide at least two methods, one is used to create our token, and the other is used to obtain Authentication based on token.
The provider needs to ensure that the Key key is unique and built using init(), otherwise an exception will be thrown.
@Component
@Slf4j
public class JWTProvider {
private Key key; // 私钥
private long tokenValidityInMilliseconds; // 有效时间
private long tokenValidityInMillisecondsForRememberMe; // 记住我有效时间
@Autowired
private JJWTProperties jjwtProperties; // jwt配置参数
@Autowired
private UserRepository userRepository;
@PostConstruct
public void init() {
byte[] keyBytes;
String secret = jjwtProperties.getSecret();
if (StringUtils.hasText(secret)) {
log.warn("Warning: the JWT key used is not Base64-encoded. " +
"We recommend using the `jhipster.security.authentication.jwt.base64-secret` key for optimum security.");
keyBytes = secret.getBytes(StandardCharsets.UTF_8);
} else {
log.debug("Using a Base64-encoded JWT secret key");
keyBytes = Decoders.BASE64.decode(jjwtProperties.getBase64Secret());
}
this.key = Keys.hmacShaKeyFor(keyBytes); // 使用mac-sha算法的密钥
this.tokenValidityInMilliseconds =
1000 * jjwtProperties.getTokenValidityInSeconds();
this.tokenValidityInMillisecondsForRememberMe =
1000 * jjwtProperties.getTokenValidityInSecondsForRememberMe();
}
public String createToken(Authentication authentication, boolean rememberMe) {
long now = (new Date()).getTime();
Date validity;
if (rememberMe) {
validity = new Date(now + this.tokenValidityInMillisecondsForRememberMe);
} else {
validity = new Date(now + this.tokenValidityInMilliseconds);
}
User user = userRepository.findOneByLogin(authentication.getName());
Map<String ,Object> map = new HashMap<>();
map.put("sub",authentication.getName());
map.put("user",user);
return Jwts.builder()
.setClaims(map) // 添加body
.signWith(key, SignatureAlgorithm.HS512) // 指定摘要算法
.setExpiration(validity) // 设置有效时间
.compact();
}
public Authentication getAuthentication(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token).getBody(); // 根据token获取body
User principal;
Collection<? extends GrantedAuthority> authorities;
principal = userRepository.findOneByLogin(claims.getSubject());
authorities = principal.getAuthorities();
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}
}
Note that the User we created here needs to implement the UserDetails object, so that we can
principal.getAuthorities()
obtain permissions based on it. If UserDetails is not implemented, then we need to customize the authorities and add them to the UsernamePasswordAuthenticationToken.
@Data
@Entity
@Table(name="user")
public class User implements UserDetails {
@Id
@Column
private Long id;
@Column
private String login;
@Column
private String password;
@Column
private String role;
@Override
// 获取权限,这里就用简单的方法
// 在spring security中,Authorities既可以是ROLE也可以是Authorities
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton(new SimpleGrantedAuthority(role));
}
@Override
public String getUsername() {
return login;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
Create a successful login and successful logout handler
Send jwt to the foreground after successful login.
Authentication is successful, jwt is returned:
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler{
void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException{
PrintWriter writer = response.getWriter();
writer.println(jwtProvider.createToken(authentication, true));
}
}
Logout succeeded:
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
void onLogoutSuccess(HttpServletRequest var1, HttpServletResponse var2, Authentication var3) throws IOException, ServletException{
PrintWriter writer = response.getWriter();
writer.println("logout success");
writer.flush();
}
}
Set login, logout, cancel csrf protection
Logout cannot invalidate the token, you can use the database to save the token, and then delete the token when you log out.
@Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
// code...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// code...
// 添加登录处理器
.formLogin().loginProcessingUrl("/login").successHandler((request, response, authentication) -> {
PrintWriter writer = response.getWriter();
writer.println(jwtProvider.createToken(authentication, true));
})
// 取消csrf防护
.and().csrf().disable()
// code...
// 添加登出处理器
.and().logout().logoutUrl("/logout").logoutSuccessHandler((HttpServletRequest request, HttpServletResponse response, Authentication authentication) -> {
PrintWriter writer = response.getWriter();
writer.println("logout success");
writer.flush();
})
// code...
}
// code...
}
Integrate spring-security with JWT
Add Filter for spring-security to parse the token, and add our user information to the securityContext.
Before UsernamePasswordAuthenticationFilter.class, we need to add authentication based on token. The key method is to get authentication from jwt and add it to securityContext.
In SecurityConfiguration, you need to set the location where Filter is added.
Create a custom Filter for jwt to obtain authentication:
@Slf4j
public class JWTFilter extends GenericFilterBean {
private final static String HEADER_AUTH_NAME = "auth";
private JWTProvider jwtProvider;
public JWTFilter(JWTProvider jwtProvider) {
this.jwtProvider = jwtProvider;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String authToken = httpServletRequest.getHeader(HEADER_AUTH_NAME);
if (StringUtils.hasText(authToken)) {
// 从自定义tokenProvider中解析用户
Authentication authentication = this.jwtProvider.getAuthentication(authToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
// 调用后续的Filter,如果上面的代码逻辑未能复原“session”,SecurityContext中没有想过信息,后面的流程会检测出"需要登录"
filterChain.doFilter(servletRequest, servletResponse);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
Add Filter to HttpSecurity and set Filter location:
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
// code...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
//设置添加Filter和位置
.and().addFilterBefore(new JWTFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class);
// code...
}
// code...
}
MySecurityConfiguration code
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JWTProvider jwtProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)// 设置自定义的userDetailsService
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)//设置无状态
.and()
.authorizeRequests() // 配置请求权限
.antMatchers("/product/**").hasRole("USER") // 需要角色
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated() // 所有的请求都需要登录
.and()
// 配置登录url,和登录成功处理器
.formLogin().loginProcessingUrl("/login").successHandler((request, response, authentication) -> {
PrintWriter writer = response.getWriter();
writer.println(jwtProvider.createToken(authentication, true));
})
// 取消csrf防护
.and().csrf().disable()
.httpBasic()
// 配置登出url,和登出成功处理器
.and().logout().logoutUrl("/logout")
.logoutSuccessHandler((HttpServletRequest request, HttpServletResponse response, Authentication authentication) -> {
PrintWriter writer = response.getWriter();
writer.println("logout success");
writer.flush();
})
// 在UsernamePasswordAuthenticationFilter之前执行我们添加的JWTFilter
.and().addFilterBefore(new JWTFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
public void configure(WebSecurity web) {
// 添加不做权限的URL
web.ignoring()
.antMatchers("/swagger-resources/**")
.antMatchers("/swagger-ui.html")
.antMatchers("/webjars/**")
.antMatchers("/v2/**")
.antMatchers("/h2-console/**");
}
}
Use annotations to manage permissions on methods
Notes need to be
MySecurityConfiguration
added , prePostEnabled is false by default, and it needs to be set to true to enable global annotation permission control.@EnableGlobalMethodSecurity(prePostEnabled = true)
After prePostEnabled is set to true, four annotations can be used:
Add entity class School:
@Data
public class School implements Serializable {
private Long id;
private String name;
private String address;
}
- @PreAuthorize
Permission judgment before access
@RestController
public class AnnoController {
@Autowired
private JWTProvider jwtProvider;
@RequestMapping("/annotation")
// @PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public String info(){
return "拥有admin权限";
}
}
Both hasRole and hasAuthority will judge the getAuthorities in UserDetails. The difference is that hasRole will add the fields ROLE_
before making judgments. If it is used in the above example hasRole('ADMIN')
, then it will be used ROLE_ADMIN
for judgment. If it is hasAuthority('ADMIN')
, then it will be used ADMIN
for judgment.
- @PostAuthorize
Judging after the request, if the return value does not meet the conditions, an exception will be thrown, but the method itself has already been executed.
@RequestMapping("/postAuthorize")
@PreAuthorize("hasRole('ADMIN')")
@PostAuthorize("returnObject.id%2==0")
public School postAuthorize(Long id) {
School school = new School();
school.setId(id);
return school;
}
returnObject is a built-in object that refers to the return value of the method.
If returnObject.id%2==0
true, return the method value. If false, returns 403 Forbidden.
- @PreFilter
Used to filter values in a collection before method execution.
@RequestMapping("/preFilter")
@PreAuthorize("hasRole('ADMIN')")
@PreFilter("filterObject%2==0")
public List<Long> preFilter(@RequestParam("ids") List<Long> ids) {
return ids;
}
filterObject
It is a built-in object that refers to the generic class in the collection. If there are multiple collections, it needs to be specified filterTarget
.
@PreFilter(filterTarget="ids", value="filterObject%2==0")
public List<Long> preFilter(@RequestParam("ids") List<Long> ids,@RequestParam("ids") List<User> users,) {
return ids;
}
filterObject%2==0
The values in the collection will be filtered, and the value of true will be retained.
The value returned by the first example is filtered before execution to return 2, 4.
- @PostFilter
The returned collection is filtered.
@RequestMapping("/postFilter")
@PreAuthorize("hasRole('ADMIN')")
@PostFilter("filterObject.id%2==0")
public List<School> postFilter() {
List<School> schools = new ArrayList<School>();
School school;
for (int i = 0; i < 10; i++) {
school = new School();
school.setId((long)i);
schools.add(school);
}
return schools;
}
The above method returns results: School objects with ids 0, 2, 4, 6, and 8.
7. Principle explanation
1. Calibration flow chart
2. Source code analysis
- AbstractAuthenticationProcessingFilter abstract class
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authResult);
}
Call requiresAuthentication(HttpServletRequest, HttpServletResponse) to determine whether authentication is required . If authentication is required, the attemptAuthentication(HttpServletRequest, HttpServletResponse) method will be called, with three results:
- Returns an Authentication object. The configured SessionAuthenticationStrategy` will be called, and then the successfulAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, Authentication) method will be called.
- An AuthenticationException occurred while authenticating. The unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException) method will be called.
- Returns Null, indicating incomplete authentication. The method will return immediately, assuming the subclass has done some necessary work (such as redirection) to continue processing validation. It is assumed that the latter request will be received by this method, where the returned Authentication object is not null.
- UsernamePasswordAuthenticationFilter(AbstractAuthenticationProcessingFilter的子类)
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
The attemptAuthentication () method generates a UsernamePasswordAuthenticationToken object from the username and password in the request , which is used for AuthenticationManager verification (ie this.getAuthenticationManager().authenticate(authRequest)). By default the AuthenticationManager injected into the Spring container is the ProviderManager.
- ProviderManager (the implementation class of AuthenticationManager)
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] {
toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
Attempts to authenticate the Authentication object . The list of AuthenticationProviders will be tried consecutively until the AuthenticationProvider indicates that it can authenticate the passed Authentication object. Authentication will then be attempted using that AuthenticationProvider. If more than one AuthenticationProvider supports authenticating the passed Authentication object, the first one determines the result, overriding any possible AuthenticationException raised by earlier supporting AuthenticationProviders. After successful authentication, subsequent AuthenticationProviders will not be attempted. If at last all AuthenticationProviders fail to authenticate the Authentication object successfully, an AuthenticationException will be thrown. It is not difficult to see from the code that authentication is verified by the provider, and the core point method is:
Authentication result = provider.authenticate(authentication);
The provider here is AbstractUserDetailsAuthenticationProvider, AbstractUserDetailsAuthenticationProvider is the implementation of AuthenticationProvider, look at its authenticate(authentication) method:
// 验证 authentication
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
AbstractUserDetailsAuthenticationProvider has a built-in caching mechanism. If the UserDetails information cannot be obtained from the cache, the following method is called to obtain the user information , and then compared with the information sent by the user to determine whether the authentication is successful.
// 获取用户信息
UserDetails user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
The retrieveUser() method is implemented in DaoAuthenticationProvider, which is a subclass of AbstractUserDetailsAuthenticationProvider. The specific implementation is as follows:
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
UserDetails loadedUser;
try {
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
}
catch (UsernameNotFoundException notFound) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,
presentedPassword, null);
}
throw notFound;
}
catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(
repositoryProblem.getMessage(), repositoryProblem);
}
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
It can be seen that the returned object userDetails here is obtained by #loadUserByUsername(username) of UserDetailsService.
Eight, play custom login
1. Form login process
The following is the basic process of form login:
As long as it is a form login, it can basically be transformed into the above process. Next, let's see how Spring Security handles it.
3. Login in Spring Security
By default it provides three login methods:
formLogin()
normal form loginoauth2Login()
Based onOAuth2.0
authentication /authorization protocolsopenidLogin()
Based onOpenID
the identity authentication specification
The above three methods are all AbstractAuthenticationFilterConfigurer
realized ,
4. Form form login in HttpSecurity
There are two ways to enable form login. One HttpSecurity
is apply(C configurer)
to construct AbstractAuthenticationFilterConfigurer
an . This is a more advanced way of playing. The other is our HttpSecurity
common formLogin()
method to customize FormLoginConfigurer
. Let's start with the more conventional second one.
4.1 FormLoginConfigurer
This class is the configuration class for form form login. It provides some of our commonly used configuration methods:
- loginPage(String loginPage) : The login page is not an interface. For the front and rear separation mode, we need to modify it by default
/login
. - loginProcessingUrl(String loginProcessingUrl) The actual form submits user information to the background , and then is intercepted and processed
Action
by the filter , which does not actually process any logic.UsernamePasswordAuthenticationFilter
Action
- usernameParameter(String usernameParameter) is used to customize the user parameter name, default
username
. - passwordParameter(String passwordParameter) is used to customize user password name, default
password
- failureUrl(String authenticationFailureUrl) will be redirected to this path after login failure, and it is generally not used for front and back separation.
- failureForwardUrl(String forwardUrl) Login failures will be forwarded here, and it is generally used separately before and after. You can define a
Controller
(controller) to handle the return value, but be carefulRequestMethod
. - defaultSuccessUrl(String defaultSuccessUrl, boolean alwaysUse) By default, you will be redirected to this after successful login.
alwaysUse
Iftrue
Yes, you will always be redirected to this as long as the authentication process is successful. Generally recommended defaultfalse
- The effect of successForwardUrl(String forwardUrl) is the same as
defaultSuccessUrl
above butalwaysUse
be careful .true
RequestMethod
- successHandler (AuthenticationSuccessHandler successHandler) custom authentication success handler, which can replace all the above
success
methods - failureHandler (AuthenticationFailureHandler authenticationFailureHandler) custom failure success handler, which can replace all the above
failure
methods - permitAll(boolean permitAll) form form login whether to let go
Knowing this, we can create a customized login.
5. Spring Security aggregated login practice
Next is our most exciting actual login operation. If you have any doubts, you can carefully read a series of warm-up articles on Spring combat .
5.1 Simple requirements
Our interface access must be authenticated, and an error message (json) will be returned after a login error. After success, the front desk can obtain the corresponding database user information (json) (remember to desensitize in actual combat ).
We define a controller that handles success and failure:
@RestController
@RequestMapping("/login")
public class LoginController {
@Resource
private SysUserService sysUserService;
/**
* 登录失败返回 401 以及提示信息.
*
* @return the rest
*/
@PostMapping("/failure")
public Rest loginFailure() {
return RestBody.failure(HttpStatus.UNAUTHORIZED.value(), "登录失败了,老哥");
}
/**
* 登录成功后拿到个人信息.
*
* @return the rest
*/
@PostMapping("/success")
public Rest loginSuccess() {
// 登录成功后用户的认证信息 UserDetails会存在 安全上下文寄存器 SecurityContextHolder 中
User principal = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String username = principal.getUsername();
SysUser sysUser = sysUserService.queryByUsername(username);
// 脱敏
sysUser.setEncodePassword("[PROTECT]");
return RestBody.okData(sysUser,"登录成功");
}
}
Then we customize the configuration override void configure(HttpSecurity http)
method to configure as follows (crsf needs to be disabled here):
@Configuration
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class CustomSpringBootWebSecurityConfiguration {
@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER)
static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.cors()
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/process")
.successForwardUrl("/login/success").
failureForwardUrl("/login/failure");
}
}
}
Using Postman or other tools to submit the Post form http://localhost:8080/process?username=Felordcn&password=12345
will return user information:
{
"httpStatus": 200,
"data": {
"userId": 1,
"username": "Felordcn",
"encodePassword": "[PROTECT]",
"age": 18
},
"msg": "登录成功",
"identifier": ""
}
After changing the password to another value and failing to request authentication again:
{
"httpStatus": 401,
"data": null,
"msg": "登录失败了,老哥",
"identifier": "-9999"
}
6. Simple implementation of multiple login methods
Is that the end? There are many tricks to log in now. Conventional ones include text messages, emails, and code scanning. The third party is beyond the scope of today's scope that I will talk about in the future. How to deal with product managers with many ideas? Let's make a login method that can expand various postures. We can add an adapter between the user and the judgment in the above 2. form login process to adapt. We know this so-called judgment is .UsernamePasswordAuthenticationFilter
We only need to ensure that the uri is /process configured above and the user name and password can be obtained through getParameter(String name) .
I suddenly felt that I could DelegatingPasswordEncoder
imitate the method of maintaining a registry to implement different processing strategies. Of course we have to implement GenericFilterBean
a UsernamePasswordAuthenticationFilter
before execution. At the same time, develop a login strategy.
6.1 Definition of login method
Defines the login method enum ``.
public enum LoginTypeEnum {
/**
* 原始登录方式.
*/
FORM,
/**
* Json 提交.
*/
JSON,
/**
* 验证码.
*/
CAPTCHA
}
6.2 Define preprocessor interface
Define the pre-processor interface to process the received login parameters of various characteristics and process specific logic. This excuse is actually a bit random, the important thing is that you have to learn how to think. I have implemented a default form' 表单登录 和 通过
RequestBody 放入
json` in two ways, which will not be shown here due to space limitations. See the bottom for the specific DEMO .
public interface LoginPostProcessor {
/**
* 获取 登录类型
*
* @return the type
*/
LoginTypeEnum getLoginTypeEnum();
/**
* 获取用户名
*
* @param request the request
* @return the string
*/
String obtainUsername(ServletRequest request);
/**
* 获取密码
*
* @param request the request
* @return the string
*/
String obtainPassword(ServletRequest request);
}
6.3 Implement login pre-processing filter
This filter maintains LoginPostProcessor
a mapping table. The front end is used to determine the login method for policy preprocessing, and it will eventually be handed over UsernamePasswordAuthenticationFilter
. Prepended by the method HttpSecurity
of .addFilterBefore(preLoginFilter, UsernamePasswordAuthenticationFilter.class)
package cn.felord.spring.security.filter;
import cn.felord.spring.security.enumation.LoginTypeEnum;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import static org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY;
import static org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY;
/**
* 预登录控制器
*
* @author Felordcn
* @since 16 :21 2019/10/17
*/
public class PreLoginFilter extends GenericFilterBean {
private static final String LOGIN_TYPE_KEY = "login_type";
private RequestMatcher requiresAuthenticationRequestMatcher;
private Map<LoginTypeEnum, LoginPostProcessor> processors = new HashMap<>();
public PreLoginFilter(String loginProcessingUrl, Collection<LoginPostProcessor> loginPostProcessors) {
Assert.notNull(loginProcessingUrl, "loginProcessingUrl must not be null");
requiresAuthenticationRequestMatcher = new AntPathRequestMatcher(loginProcessingUrl, "POST");
LoginPostProcessor loginPostProcessor = defaultLoginPostProcessor();
processors.put(loginPostProcessor.getLoginTypeEnum(), loginPostProcessor);
if (!CollectionUtils.isEmpty(loginPostProcessors)) {
loginPostProcessors.forEach(element -> processors.put(element.getLoginTypeEnum(), element));
}
}
private LoginTypeEnum getTypeFromReq(ServletRequest request) {
String parameter = request.getParameter(LOGIN_TYPE_KEY);
int i = Integer.parseInt(parameter);
LoginTypeEnum[] values = LoginTypeEnum.values();
return values[i];
}
/**
* 默认还是Form .
*
* @return the login post processor
*/
private LoginPostProcessor defaultLoginPostProcessor() {
return new LoginPostProcessor() {
@Override
public LoginTypeEnum getLoginTypeEnum() {
return LoginTypeEnum.FORM;
}
@Override
public String obtainUsername(ServletRequest request) {
return request.getParameter(SPRING_SECURITY_FORM_USERNAME_KEY);
}
@Override
public String obtainPassword(ServletRequest request) {
return request.getParameter(SPRING_SECURITY_FORM_PASSWORD_KEY);
}
};
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ParameterRequestWrapper parameterRequestWrapper = new ParameterRequestWrapper((HttpServletRequest) request);
if (requiresAuthenticationRequestMatcher.matches((HttpServletRequest) request)) {
LoginTypeEnum typeFromReq = getTypeFromReq(request);
LoginPostProcessor loginPostProcessor = processors.get(typeFromReq);
String username = loginPostProcessor.obtainUsername(request);
String password = loginPostProcessor.obtainPassword(request);
parameterRequestWrapper.setAttribute(SPRING_SECURITY_FORM_USERNAME_KEY, username);
parameterRequestWrapper.setAttribute(SPRING_SECURITY_FORM_PASSWORD_KEY, password);
}
chain.doFilter(parameterRequestWrapper, response);
}
}
6.4 Verification
The request can be successful through POST
the form submission method http://localhost:8080/process?username=Felordcn&password=12345&login_type=0
. Or submit successfully in the following ways:
More ways only need to implement interface LoginPostProcessor
injectionPreLoginFilter
Nine integration JWT for login authentication
JWT is the abbreviation of JSON Web Token, which is currently the most popular cross-domain authentication solution.
The general process of Internet service certification is:
- The user sends the account number and password to the server
- After the server is authenticated, save the user's role, login time and other information in the current session
- At the same time, the server returns a session_id to the user (usually stored in a cookie)
- When the user sends the request again, the cookie containing the session_id is sent to the server
- The server receives the session_id, finds the session, and extracts user information
The above authentication mode has the following disadvantages:
- Cookies do not allow cross-domain
- Because each server must save the session object, the scalability is not good
The principle of JWT authentication is:
- The user sends the account number and password to the server
- After the server is authenticated, generate a token token and return it to the client (token can contain user information)
- When the user requests again, put the token in the request
Authorization
header - After receiving the request, the server verifies that the token is legal and releases the request
The JWT token token can contain information such as user identity and login time, so that the login status holder changes from the server to the client, and the server becomes stateless; the token is placed in the request header to achieve cross-domain
Composition of JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT consists of three parts:
- Header
- Payload
- Signature
The form of expression is:Header.Payload.Signature
The Header part is a JSON object that describes the metadata of the JWT, usually as follows:
{
"alg": "HS256",
"typ": "JWT"
}
In the above code, alg
the attribute indicates the signature algorithm (algorithm), the default is HMAC SHA256 (written as HS256); the typ
attribute indicates the type of the token (token), and the JWT token is uniformly written as JWT
.
The above JSON object is converted into a string using the Base64URL algorithm
Payload
The Payload part is also a JSON object, which is used to store the actual data that needs to be passed. JWT specifies 7 official fields:
- iss (issuer): Issuer
- exp (expiration time): expiration time
- sub (subject): subject
- aud (audience): audience
- nbf (Not Before): effective time
- iat (Issued At): Issue time
- jti (JWT ID): number
Of course, users can also define private fields.
This JSON object should also be converted into a string using the Base64URL algorithm
Signature
The Signature part is the signature of the first two parts to prevent data tampering
The signature algorithm is as follows:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)
After calculating the signature, combine the three parts of Header, Payload and Signature into a string, and separate each part with "."
JWT authentication and authorization
Security is a security framework based on AOP and Servlet filters. In order to implement JWT to rewrite those methods and customize those filters, you need to first understand the filters that come with security. The security default filter chain is as follows:
- org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
- org.springframework.security.web.context.SecurityContextPersistenceFilter
- org.springframework.security.web.header.HeaderWriterFilter
- org.springframework.security.web.authentication.logout.LogoutFilter
- org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
- org.springframework.security.web.savedrequest.RequestCacheAwareFilter
- org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
- org.springframework.security.web.authentication.AnonymousAuthenticationFilter
- org.springframework.security.web.session.SessionManagementFilter
- org.springframework.security.web.access.ExceptionTranslationFilter
- org.springframework.security.web.access.intercept.FilterSecurityInterceptor
SecurityContextPersistenceFilter
This filter does two things:
- When the user sends a request, extract the user information from the session object and save it in the securitycontext of the SecurityContextHolder
- At the end of the current request response, put the user information saved in the securitycontext of the SecurityContextHolder into the session, so as to share data in the next request; at the same time, clear the securitycontext of the SecurityContextHolder
Since the session function is disabled, the filter has only one function left, which is to clear the securitycontext of the SecurityContextHolder. To illustrate why the securitycontext should be cleared: user 1 sends a request, which is processed by thread M, and when the response is completed, thread M puts it back into the thread pool; user 2 sends a request, and this request is also processed by thread M. Since the securitycontext is not cleared, it should The information of user 2 is stored but the information of user 1 is stored at this time, causing the user information to be inconsistent
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter
Inherited from AbstractAuthenticationProcessingFilter
, the processing logic is doFilter
in the method:
- When the request is
UsernamePasswordAuthenticationFilter
intercepted, judge whether the request path matches the login URL, if not, continue to execute the next filter; otherwise, execute step 2 - Call
attemptAuthentication
method for authentication.UsernamePasswordAuthenticationFilter
The method is rewrittenattemptAuthentication
, responsible for reading form login parameters, entrustingAuthenticationManager
authentication, and returning an authenticated token (null means authentication failed) - Determine whether the token is null, non-null means authentication is successful, null means authentication failed
- Called if the authentication is successful
successfulAuthentication
. This method puts the authenticated token into the securitycontext for subsequent request authorization. At the same time, this method reserves an extension point (AuthenticationSuccessHandler.onAuthenticationSuccess方法
) for processing after successful authentication - If the authentication fails, it can also be extended
uthenticationFailureHandler.onAuthenticationFailure
to process after the authentication fails - As long as the current request path matches the login URL, no matter whether the authentication succeeds or fails, the current request will be completed and the filter chain will not be executed.
UsernamePasswordAuthenticationFilter
method attemptAuthentication
, the execution logic is as follows:
- Get form parameters from the request. Because
HttpServletRequest.getParameter
the method is used to obtain parameters, it can only handle requests whose Content-Type is application/x-www-form-urlencoded or multipart/form-data. If it is application/json, the value cannot be obtained - Encapsulate the account and password obtained in step 1 into
UsernamePasswordAuthenticationToken
an object, and create an unauthenticated token.UsernamePasswordAuthenticationToken
There are two overloaded construction methods, whichpublic UsernamePasswordAuthenticationToken(Object principal, Object credentials)
create an unauthenticated token andpublic UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities)
create an authenticated token - Get the authentication manager
AuthenticationManager
, whose default implementation isProviderManager
, call itauthenticate
for authentication ProviderManager
Itauthenticate
is a template method, it traverses allAuthenticationProvider
until it finds a certain type of token that supports authenticationAuthenticationProvider
, callsAuthenticationProvider.authenticate
the method authentication,AuthenticationProvider.authenticate
loads the correct account number and password for comparison and verificationAuthenticationManager.authenticate
The method returns an authenticated token
AnonymousAuthenticationFilter
AnonymousAuthenticationFilter
Responsible for creating anonymous tokens:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
if (SecurityContextHolder.getContext().getAuthentication() == null) {
SecurityContextHolder.getContext().setAuthentication(this.createAuthentication((HttpServletRequest)req));
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.of(() -> {
return "Set SecurityContextHolder to " + SecurityContextHolder.getContext().getAuthentication();
}));
} else {
this.logger.debug("Set SecurityContextHolder to anonymous SecurityContext");
}
} else if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.of(() -> {
return "Did not set SecurityContextHolder since already authenticated " + SecurityContextHolder.getContext().getAuthentication();
}));
}
chain.doFilter(req, res);
}
If the current user is not authenticated, an anonymous token will be created, and whether the user can read the resource FilterSecurityInterceptor
will be entrusted to the decision manager by the filter to determine whether it has permission to read
Implementation ideas
JWT authentication ideas:
- Use Security's native form authentication filter to verify username and password
- After the verification is passed, customize
AuthenticationSuccessHandler
the authentication success processor, and the token token will be generated by the processor
JWT authorization idea:
- The purpose of using JWT is to make the server stateless and not share data with sessions, so the session function of security should be disabled (http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS))
- When designing the token token data structure, the payload part should store user name and role information
- The token token has two functions:
- Authentication, the token sent by the user is legal, which means the authentication is successful
- Authorization, extract the role information after the token verification is successful, construct the authenticated token, put it into the securitycontext, and hand over the specific authority judgment to the security framework
- Implement a filter by yourself, intercept user requests, and implement the functions mentioned in (3)
Code implementation to create JWT tool class
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.12.0</version>
</dependency>
We encapsulate the API provided by java-jwt to facilitate the creation, verification and extraction of claims
@Slf4j
public class JWTUtil {
// 携带token的请求头名字
public final static String TOKEN_HEADER = "Authorization";
//token的前缀
public final static String TOKEN_PREFIX = "Bearer ";
// 默认密钥
public final static String DEFAULT_SECRET = "mySecret";
// 用户身份
private final static String ROLES_CLAIM = "roles";
// token有效期,单位分钟;
private final static long EXPIRE_TIME = 5 * 60 * 1000;
// 设置Remember-me功能后的token有效期
private final static long EXPIRE_TIME_REMEMBER = 7 * 24 * 60 * 60 * 1000;
// 创建token
public static String createToken(String username, List role, String secret, boolean rememberMe) {
Date expireDate = rememberMe ? new Date(System.currentTimeMillis() + EXPIRE_TIME_REMEMBER) : new Date(System.currentTimeMillis() + EXPIRE_TIME);
try {
// 创建签名的算法实例
Algorithm algorithm = Algorithm.HMAC256(secret);
String token = JWT.create()
.withExpiresAt(expireDate)
.withClaim("username", username)
.withClaim(ROLES_CLAIM, role)
.sign(algorithm);
return token;
} catch (JWTCreationException jwtCreationException) {
log.warn("Token create failed");
return null;
}
}
// 验证token
public static boolean verifyToken(String token, String secret) {
try{
Algorithm algorithm = Algorithm.HMAC256(secret);
// 构建JWT验证器,token合法同时pyload必须含有私有字段username且值一致
// token过期也会验证失败
JWTVerifier verifier = JWT.require(algorithm)
.build();
// 验证token
DecodedJWT decodedJWT = verifier.verify(token);
return true;
} catch (JWTVerificationException jwtVerificationException) {
log.warn("token验证失败");
return false;
}
}
// 获取username
public static String getUsername(String token) {
try {
// 因此获取载荷信息不需要密钥
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException jwtDecodeException) {
log.warn("提取用户姓名时,token解码失败");
return null;
}
}
public static List<String> getRole(String token) {
try {
// 因此获取载荷信息不需要密钥
DecodedJWT jwt = JWT.decode(token);
// asList方法需要指定容器元素的类型
return jwt.getClaim(ROLES_CLAIM).asList(String.class);
} catch (JWTDecodeException jwtDecodeException) {
log.warn("提取身份时,token解码失败");
return null;
}
}
}
code implementation certification
Verify the account number and password UsernamePasswordAuthenticationFilter
, without modifying the code
After the authentication is successful, a token needs to be generated and returned to the client, which we AuthenticationSuccessHandler.onAuthenticationSuccess方法
implement through extensions
@Component
public class JWTAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
ResponseData responseData = new ResponseData();
responseData.setCode("200");
responseData.setMessage("登录成功!");
// 提取用户名,准备写入token
String username = authentication.getName();
// 提取角色,转为List<String>对象,写入token
List<String> roles = new ArrayList<>();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities){
roles.add(authority.getAuthority());
}
// 创建token
String token = JWTUtil.createToken(username, roles, JWTUtil.DEFAULT_SECRET, true);
httpServletResponse.setCharacterEncoding("utf-8");
// 为了跨域,把token放到响应头WWW-Authenticate里
httpServletResponse.setHeader("WWW-Authenticate", JWTUtil.TOKEN_PREFIX + token);
// 写入响应里
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(httpServletResponse.getWriter(), responseData);
}
}
In order to unify the return value, we encapsulate an ResponseData
object
Code Implementation Authorization
Customize a filter JWTAuthorizationFilter
to verify the token. After the token verification is successful, the authentication is considered successful
@Slf4j
public class JWTAuthorizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = getTokenFromRequestHeader(request);
Authentication verifyResult = verefyToken(token, JWTUtil.DEFAULT_SECRET);
if (verifyResult == null) {
// 即便验证失败,也继续调用过滤链,匿名过滤器生成匿名令牌
chain.doFilter(request, response);
return;
} else {
log.info("token令牌验证成功");
SecurityContextHolder.getContext().setAuthentication(verifyResult);
chain.doFilter(request, response);
}
}
// 从请求头获取token
private String getTokenFromRequestHeader(HttpServletRequest request) {
String header = request.getHeader(JWTUtil.TOKEN_HEADER);
if (header == null || !header.startsWith(JWTUtil.TOKEN_PREFIX)) {
log.info("请求头不含JWT token, 调用下个过滤器");
return null;
}
String token = header.split(" ")[1].trim();
return token;
}
// 验证token,并生成认证后的token
private UsernamePasswordAuthenticationToken verefyToken(String token, String secret) {
if (token == null) {
return null;
}
// 认证失败,返回null
if (!JWTUtil.verifyToken(token, secret)) {
return null;
}
// 提取用户名
String username = JWTUtil.getUsername(token);
// 定义权限列表
List<GrantedAuthority> authorities = new ArrayList<>();
// 从token提取角色
List<String> roles = JWTUtil.getRole(token);
for (String role : roles) {
log.info("用户身份是:" + role);
authorities.add(new SimpleGrantedAuthority(role));
}
// 构建认证过的token
return new UsernamePasswordAuthenticationToken(username, null, authorities);
}
}
OncePerRequestFilter`保证当前请求中,此过滤器只被调用一次,执行逻辑在`doFilterInternal
Code implementation security configuration
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AjaxAuthenticationEntryPoint ajaxAuthenticationEntryPoint;
@Autowired
private JWTAuthenticationSuccessHandler jwtAuthenticationSuccessHandler;
@Autowired
private AjaxAuthenticationFailureHandler ajaxAuthenticationFailureHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.successHandler(jwtAuthenticationSuccessHandler)
.failureHandler(ajaxAuthenticationFailureHandler)
.permitAll()
.and()
.addFilterAfter(new JWTAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling().authenticationEntryPoint(ajaxAuthenticationEntryPoint);
}
}
The session function is canceled in the configuration, and the filter we defined is added to the filter chain; at the same time, ajaxAuthenticationEntryPoint
the exception thrown when unauthenticated users access unauthorized resources is defined
@Component
public class AjaxAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
ResponseData responseData = new ResponseData();
responseData.setCode("401");
responseData.setMessage("匿名用户,请先登录再访问!");
httpServletResponse.setCharacterEncoding("utf-8");
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(httpServletResponse.getWriter(), responseData);
}
}
Introduction to the filter chain
In the previous section, we mainly talked about the core components and core methods of Spring Security authentication and authorization. But when are these methods called? The answer is Filter and AOP. Spring Security controls access to permissions through various interceptors when we authenticate users and grant permissions.
For the protection of endpoints based on HttpRequest, we use a Filter Chain to protect; for the protection based on method calls, we use AOP to protect. This article focuses on the types of filter chains in Spring Security and how to implement authentication and authorization in filters.
Spring Security will add 15 filters for us by default. We can see the construction process of the filter chain SecurityFilterChain from the performBuild() method of WebSecurity (WebSecurity is an important object loaded by Spring Security, which will be described in the next section), and Handed over to the proxy of the FilterChainProxy object. We can see from the log in DefaultSecurityFilterChain, the default implementation class of SecurityFilterChain, that Spring Security consists of the following filters to form a filter chain:
Creating filter chain: any request, [
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@7f353a0f,
org.springframework.security.web.context.SecurityContextPersistenceFilter@4735d6e5,
org.springframework.security.web.header.HeaderWriterFilter@314a31b0,
org.springframework.security.web.csrf.CsrfFilter@4ef2ab73,
org.springframework.security.web.authentication.logout.LogoutFilter@57efc6fd,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@d88f893,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@2cd388f5,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7ea2412c,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@2091833,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@4dad0eed,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@16132f21,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@1c93b51e,
org.springframework.security.web.session.SessionManagementFilter@59edb4f5,
org.springframework.security.web.access.ExceptionTranslationFilter@104dc1a2,
org.springframework.security.web.access.intercept.FilterSecurityInterceptor@1de0641b
]
The following is the function of each filter,
Among them, SecurityContextPersistenceFilter, UsernamePasswordAuthenticationFilter and FilterSecurityInterceptor correspond to the processing of SecurityContext, AuthenticationManager and AccessDecisionManager respectively.
[WebAsyncManagerIntegrationFilter] (asynchronously) provides integration of securityContext and WebAsyncManager.
The way is to set SecurityContext to Callable through the beforeConcurrentHandling(NativeWebRequest, Callable) method of SecurityContextCallableProcessingInterceptor.
In fact, it is to set the SecurityContext to the asynchronous thread so that it can also obtain the user context authentication information.
[SecurityContextPersistenceFilter] (synchronous method) Get information from SecurityContextRepository (the default implementation is HttpSessionSecurityContextRepository) and populate SecurityContextHolder (if not, create a new ThreadLocal SecurityContext) before the request, and clear SecurityContextHolder and update SecurityContextRepository after the request is completed.
In Spring Security, although the security context information is stored in the Session, the actual Filter should not directly operate the Session (the filter is generally responsible for the core processing flow, and the specific business implementation is usually handed over to other entity classes aggregated in it ), but use loadContext() and saveContext() in HttpSessionSecurityContextRepository to access the session.
[HeaderWriterFilter] is used to add some Headers to the http response, such as X-Frame-Options, X-XSS-Protection*, X-Content-Type-Options.
[CsrfFilter] is enabled by default, a filter used to prevent csrf attacks
[LogoutFilter] Filter to handle logout
[UsernamePasswordAuthenticationFilter] submits username and password in the form, and is encapsulated into a UsernamePasswordAuthenticationToken object for a series of authentication, which is mainly done through this filter, that is, calling AuthenticationManager.authenticate(). In the form authentication method, this is the most critical filter. The specific process is:
(1) Call the AbstractAuthenticationProcessingFilter.doFilter() method to execute the filter
(2) Call the UsernamePasswordAuthenticationFilter.attemptAuthentication() method
(3) Call the AuthenticationManager.authenticate() method (actually entrusted to the implementation class of AuthenticationProvider to handle)
[DefaultLoginPageGeneratingFilter] & [DefaultLogoutPageGeneratingFilter] If /login and login page are not configured, the system will automatically configure these two Filters.
[BasicAuthenticationFilter] Processes a HTTP request’s BASIC authorization headers, putting the result into the SecurityContextHolder.
[RequestCacheAwareFilter] internally maintains a RequestCache for caching request requests
[SecurityContextHolderAwareRequestFilter] This filter wraps the ServletRequest so that the request has a richer API (populates the ServletRequest with a request wrapper which implements servlet API security methods)
[AnonymousAuthenticationFilter] Anonymous identity filter, in order to be compatible with unlogged access, spring security has also gone through a set of authentication processes, which is just an anonymous identity. It is located after the identity authentication filter (eg UsernamePasswordAuthenticationFilter), which means that the filter of AnonymousAuthenticationFilter will be meaningful only after the above identity filter is executed and the SecurityContext still has no user information.
[SessionManagementFilter] and session-related filters maintain a SessionAuthenticationStrategy internally to perform any session-related activities, such as session-fixation protection mechanisms or checking for multiple concurrent logins.
[ExceptionTranslationFilter] exception translation filter, this filter itself does not handle exceptions, but the exceptions (AccessDeniedException and AuthenticationException) that occur during the authentication process are handed over to some classes maintained internally for processing. It
is located behind the entire springSecurityFilterChain and is used to convert the exceptions that appear in the entire link and convert them. As the name suggests, the conversion means that it does not process itself. Generally, it only handles two types of exceptions: AccessDeniedException and AuthenticationException.
It connects exceptions in Java and HTTP responses together, so that when handling exceptions, we don't have to think about what page to jump to when the password is wrong, or how to lock the account. We only need to pay attention to our own business logic and throw corresponding exceptions You can. If the filter detects an AuthenticationException, it will be handed over to the internal AuthenticationEntryPoint for processing. If an AccessDeniedException is detected, it is necessary to first determine whether the current user is an anonymous user. If it is an anonymous access, run the AuthenticationEntryPoint as before, otherwise it will be delegated to AccessDeniedHandler to process, and the default implementation of AccessDeniedHandler is AccessDeniedHandlerImpl.
[FilterSecurityInterceptor] This filter determines the permissions that access to a specific path should have, and what permissions or roles are required for these restricted resource accesses. These judgments and processing are all performed by this class.
(1) Call the FilterSecurityInterceptor.invoke() method to execute the filter
(2) Call the AbstractSecurityInterceptor.beforeInvocation() method
(3) Call the AccessDecisionManager.decide() method to decide whether to have the permission
reference
JSON Web Token Introductory Tutorial
Spring Security-5-Authentication Process Combing
Spring Security3 Source Code Analysis (5)-SecurityContextPersistenceFilter Analysis
Spring Security addFilter() Sequence problem
Form Data and Request Payload of front-end and back-end joint debugging, do you really understand?
Spring Boot 2 + Spring Security 5 + JWT Single Page Application Restful Solution
SpringBoot Practical - Chapter 10 Source Code
https://www.cnblogs.com/cjsblog/p/9184173.html
https://www.cnblogs.com /storml/p/10937486.html
The Path to Technical Freedom PDF is available at:
Realize your architectural freedom:
" Have a thorough understanding of the 8-figure-1 template, everyone can do the architecture "
" 10Wqps review platform, how to structure it? This is what station B does! ! ! "
" Peak 21WQps, 100 million DAU, how is the small game "Sheep a Sheep" structured? "
" How to Scheduling 10 Billion-Level Orders, Come to a Big Factory's Superb Solution "
" Two Big Factory 10 Billion-Level Red Envelope Architecture Scheme "
… more architecture articles, being added
Realize your responsive freedom:
" Responsive Bible: 10W Words, Realize Spring Responsive Programming Freedom "
This is the old version of " Flux, Mono, Reactor Combat (the most complete in history) "
Realize your spring cloud freedom:
" Spring Cloud Alibaba Study Bible " PDF
" Sharding-JDBC underlying principle and core practice (the most complete in history) "
Realize your linux freedom:
" Linux Commands Encyclopedia: 2W More Words, One Time to Realize Linux Freedom "
Realize your online freedom:
" Detailed explanation of TCP protocol (the most complete in history) "
" Three Network Tables: ARP Table, MAC Table, Routing Table, Realize Your Network Freedom!" ! "
Realize your distributed lock freedom:
" Redis Distributed Lock (Illustration - Second Understanding - The Most Complete in History) "
" Zookeeper Distributed Lock - Diagram - Second Understanding "
Realize your king component freedom:
" King of the Queue: Disruptor Principles, Architecture, and Source Code Penetration "
" The King of Cache: The Use of Caffeine (The Most Complete in History) "
" Java Agent probe, bytecode enhanced ByteBuddy (the most complete in history) "
Realize your interview questions freely:
4000 pages of "Nin's Java Interview Collection" 40 topics
Please go to the official account of "Technical Freedom Circle" to get the PDF file updates of the above Nien architecture notes and interview questions↓↓↓