“This is my fifth day of the November Gwen Challenge.The final text challenge in 2021”.
Configuration-based authentication and authorization
- new
controller
package - Three new controller under the package, AdminController, AppController, UserController
- Create test apis separately
/** ** /
@RequestMapping("/admin/api")
@RestController
public class AdminController {
@RequestMapping(value = "/hi",method = RequestMethod.GET)
public String hi(a){
return "hi,admin."; }}Copy the code
/** * simulates the public Api */
@RequestMapping("/app/api")
@RestController
public class AppController {
@RequestMapping(value = "/hi", method = RequestMethod.GET)
public String hi(a) {
return "hi,app."; }}Copy the code
/** * simulates user related Api */
@RequestMapping("/user/api")
@RestController
public class UserController {
@RequestMapping(value = "/hi", method = RequestMethod.GET)
public String hi(a) {
return "hi,user."; }}Copy the code
- Configuring Resource Authorization
- configuration
configure
- Modify the previous configuration
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/api/**").hasRole("ADMIN")
.antMatchers("/user/api/**").hasRole("USER")
.antMatchers("/app/api/**").permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/myLogin.html")
// Specify the path to process the login request. Modify the path of the request. The default path is /login
.loginProcessingUrl("/mylogin")
// Make the login page unrestricted
.permitAll()
.and()
.csrf().disable();
}
Copy the code
antMatchers()
A usedANTPatterns ofURLmatcher?
Matches any single character*
Matches 0 or any number of characters**
Represents directories that match 0 or more- Restart the service
- accessapi
http://localhost:8080/app/api/hi
- The access success page is displayed
hi,app.
- accessapi
http://localhost:8080/user/api/hi
- The login page is displayed
- Enter a user-defined user name and password
- If the login succeeds, error 403 is reported, indicating that the authorization fails
- The authentication succeeded, but the authorization failed. Procedure
Because we configure.antmatchers (“/user/ API /**”).hasrole (” user “), which requires users to have user role permissions
- Modifying a Configuration File
application.yml
spring:
security:
user:
name: caoshenyang
password: 123456
roles: USER
Copy the code
- Add USER permission to a USER
- Restart the project
- accessapi
http://localhost:8080/user/api/hi
- After successful login, the page is displayed
hi,user.
Access API http://localhost:8080/admin/api/hi
In the same situation
Modify the configuration file application.yml
Add ADMIN permission to the user
Restart the project
The access is normal, and the page displays hi and admin.
2. Memory-based multi-user Settings
1. Implement the user-defined UserDetailsService
@Bean
public UserDetailsService userDetailsService(a){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
/ / 698 d51a19d8a121ce581499d7b701668 proclaimed in 111 after the encryption MD5 encryption
/ / it is clear
manager.createUser(User.withUsername("aa").password("{MD5}698d51a19d8a121ce581499d7b701668").roles("USER").build());
manager.createUser(User.withUsername("bb").password("{noop}222").roles("USER").build());
return manager;
}
Copy the code
Note: Encryption must be configured for springSecurity 5.x or later, the following exceptions occur
There is no PasswordEncoder mapped for the id "null"
Copy the code
The springSecurity5. x encryption mode is configured in the {Id}password format
Take a look at the encryption that comes with PasswordEncoderFactories
public class PasswordEncoderFactories {
public static PasswordEncoder createDelegatingPasswordEncoder(a) {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap".new LdapShaPasswordEncoder());
encoders.put("MD4".new Md4PasswordEncoder());
encoders.put("MD5".new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2".new Pbkdf2PasswordEncoder());
encoders.put("scrypt".new SCryptPasswordEncoder());
encoders.put("SHA-1".new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256".new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256".new StandardPasswordEncoder());
encoders.put("argon2".new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
private PasswordEncoderFactories(a) {}}Copy the code
- Restart the
- Enter your account password
- Login successful
- This configuration overrides the previous one
application.yml
The configuration in
2. Through congfigure
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.passwordEncoder(NoOpPasswordEncoder.getInstance())
.withUser("tom").password("111").roles("ADMIN"."USER")
.and()
.withUser("lisi").password("222").roles("USER");
}
Copy the code
This configuration overwrites the original application. Yml configuration and the custom UserDetailsService configuration. You can choose either one
Authorization and authentication based on the default database model
- Check the InMemoryUserDetailsManager source
- UserDetailsManager interface is realized
- Select the UserDetailsManager interface, Ctrl+H
Another implementation class, JdbcUserDetailsManager, was found to implement this interface
You can guess from the name that the implementation class connects to the database via JDBC
- Introduce JDBC and MYSQL dependencies for the project
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.20</version>
</dependency>
Copy the code
application.yml
Configure data connection parameters
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql://localhost:3306/springSecurityDemo? useUnicode=true&&characterEncoding=utf8&&useSSL=false&&serverTimezone=Asia/Shanghai
Copy the code
- Create the database springSecurityDemo
SpringSecurity provides a default database model
public JdbcUserDetailsManagerConfigurer<B> withDefaultSchema(a) {
this.initScripts.add(new ClassPathResource(
"org/springframework/security/core/userdetails/jdbc/users.ddl"));
return this;
}
Copy the code
Address in org/springframework/security/core/populated userdetails/JDBC/users. The DDL
create table users(username varchar_ignorecase(50) not null primary key.password varchar_ignorecase(500) not null,enabled boolean not null);
create table authorities (username varchar_ignorecase(50) not null,authority varchar_ignorecase(50) not null.constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);
Copy the code
Note: MySql does not support the varchar_ignorecase type. Change it to varchar
create table users(username VARCHAR(50) not null primary key.password VARCHAR(500) not null,enabled boolean not null);
create table authorities (username VARCHAR(50) not null,authority VARCHAR(50) not null.constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);
Copy the code
- Execute the construction statement
- Create two tables
Authorities table
The users table
- Build an instance of JdbcUserDetailsManager and let SpringSecurity manage users using a database, similar to memory, except that the user information comes from the database
- The introduction of the DataSource
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/api/**").hasRole("ADMIN")
.antMatchers("/user/api/**").hasRole("USER")
.antMatchers("/app/api/**").permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/myLogin.html")
// Specify the path to process the login request. Modify the path of the request. The default path is /login
.loginProcessingUrl("/mylogin")
.permitAll()
.and()
.csrf().disable();
}
/** * User Settings based on default database data model */
@Bean
public UserDetailsService userDetailsService(a){
JdbcUserDetailsManager manager = new JdbcUserDetailsManager();
manager.setDataSource(dataSource);
/ / the MD5 encryption article 111 698 d51a19d8a121ce581499d7b701668 after encryption
manager.createUser(User.withUsername("aa").password("{MD5}698d51a19d8a121ce581499d7b701668").roles("USER").build());
manager.createUser(User.withUsername("bb").password("{noop}222").roles("USER").build());
returnmanager; }}Copy the code
- Restart the project
- accessapi
http://localhost:8080/user/api/hi
- Enter a user name
aa
password111
- Access to success
The database is found to store this information
Also notice that we prefix the permissions we set with **ROLE_**
- View the JdbcUserDetailsManager source code
The discovery defines a large number of SQL execution statements
CreateUser () is equivalent to executing the following SQL statement
insert into users (username, password, enabled) values (? ,? ,?)
Copy the code
There is a problem in the above code. Whenever we restart the project, we will create a user, but username is the primary key, and we will get a primary key conflict exception
nested exception is java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'aa' for key 'PRIMARY'
Copy the code
- Make a little change
/** * User Settings based on default database data model */
@Bean
public UserDetailsService userDetailsService(a) {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager();
manager.setDataSource(dataSource);
if(! manager.userExists("aa")) {
/ / the MD5 encryption article 111 698 d51a19d8a121ce581499d7b701668 after encryption
manager.createUser(User.withUsername("aa").password("{MD5}698d51a19d8a121ce581499d7b701668").roles("USER").build());
}
if(! manager.userExists("bb")) {
manager.createUser(User.withUsername("bb").password("{noop}222").roles("USER").build());
}
return manager;
}
Copy the code
-
Restart the project
-
The normal operation
-
Add administrators by modifying database data
- accessapi
http://localhost:8080/admin/api/hi
Enter the administrator user name and password defined by yourself. The access succeeds
4. Authorization and authentication based on custom database model
In project development, the default database model was too simple to meet the needs of our business. SpringSecurity also supports authorization and authentication for custom database models.
- Let’s plug in the custom database model
- The persistence layer framework uses MyBatis-Plus
- Use the Lombok plug-in to simplify code
- Introduce dependencies for the project
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
Copy the code
1. Implement populated UserDetails
In the previous example, by implementing the UserDetailsService and annotating it into the Spring container, Spring Security will automatically discover and use it. UserDetailsService only implements a loadUserByUsername() method. Used to get the UserDetails object, which contains a list of information needed for validation
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword(a);
String getUsername(a);
boolean isAccountNonExpired(a);
boolean isAccountNonLocked(a);
boolean isCredentialsNonExpired(a);
boolean isEnabled(a);
}
Copy the code
So no matter what the data source is, or how the database structure changes, we just need to construct a UserDetails.
1.1 Implement your own user table
CREATE TABLE `t_user` (
`id` BIGINT ( 20 ) NOT NULL AUTO_INCREMENT,
`username` VARCHAR ( 60 ) NOT NULL.`password` VARCHAR ( 60 ) NOT NULL.`enable` TINYINT ( 4 ) NOT NULL DEFAULT '1' COMMENT 'User available'.`roles` text CHARACTER SET utf8mb4 COMMENT 'User roles, separated by commas',
PRIMARY KEY ( `id` ), KEY ( `username`))ENGINE = INNODB DEFAULT CHARSET = utf8mb4;
Copy the code
- Add an index to the username field to speed up search
- Insert two pieces of data manually
1.2 Writing our User entity
- create
entity
Package storage entity - Create the User entity class
@Data
public class User {
private Long id;
private String username;
private String password;
private String roles;
private boolean enable;
}
Copy the code
- Implement populated UserDetails
@Data
public class User implements UserDetails {
private Long id;
private String username;
private String password;
private String roles;
private boolean enable;
private List<GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public boolean isAccountNonExpired(a) {
return true;
}
@Override
public boolean isAccountNonLocked(a) {
return true;
}
@Override
public boolean isCredentialsNonExpired(a) {
return true;
}
@Override
public boolean isEnabled(a) {
return this.enable; }}Copy the code
Overriding methods
- IsAccountNonExpired (), isAccountNonLocked(), isCredentialsNonExpired()It won’t take all of them to returntrue
- IsEnabled () corresponds to the Enable field
- GetAuthorities () originally corresponding to the roles field, but defined its own structural changes, so we first created a new authorities and filled it in later.
1.3 Preparation of the Persistence Layer
- create
mapper
package - Create UserMapper
@Component
public interface UserMapper extends BaseMapper<User> {
@Select("SELECT * FROM t_user WHERE username = #{username}")
User findByUserName(@Param("username") String username);
}
Copy the code
- Start class to add package scan annotations
@SpringBootApplication
@MapperScan("com.yang.springsecurity.mapper")
public class SpringSecurityApplication {
public static void main(String[] args) { SpringApplication.run(SpringSecurityApplication.class, args); }}Copy the code
- Write business code
- create
service
package - Create MyUserDetailsService to implement UserDetailsService
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// Query user information
User user = userMapper.selectByUsername(username);
if (user==null) {throw new UsernameNotFoundException(username+"User does not exist");
}
// Repopulate roles
user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
returnuser; }}Copy the code
Note: Encryption must be configured for springSecurity 5.x or later, the following exceptions occur
There is no PasswordEncoder mapped for the id "null"
Copy the code
- Configure the default encryption mode
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private MyUserDetailsService myUserDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/api/**").hasRole("ADMIN")
.antMatchers("/user/api/**").hasRole("USER")
.antMatchers("/app/api/**").permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/myLogin.html")
// Specify the path to process the login request. Modify the path of the request. The default path is /login
.loginProcessingUrl("/mylogin")
.permitAll()
.and()
.csrf().disable();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(myUserDetailsService).passwordEncoder(NoOpPasswordEncoder.getInstance()); }}Copy the code
- Restart the project
- accessapi
http://localhost:8080/admin/api/hi
- Enter the user name and password
- Access to success
So far, we have realized the authorization and authentication of the customized database model. Later, we can enrich the authentication logic and strengthen security according to the needs of the project
There is always a question here, why do we need to prefix our database permissions with ROLE_?
Look at the source code for the hasRole() method
private static String hasRole(String role) {
Assert.notNull(role, "role cannot be null");
if (role.startsWith("ROLE_")) {
throw new IllegalArgumentException(
"role should not start with 'ROLE_' since it is automatically inserted. Got '"
+ role + "'");
}
return "hasRole('ROLE_" + role + "')";
}
Copy the code
If you do not want to match this prefix, use the **hasAuthority()** method instead