Spring Security is a series of simple introduction and practical

I also suffer from the fact that spring Security tutorials are so uneven that I either analyze the source code directly at the beginning or just post some code and say “this is how to configure it.” So I decided to develop a Series of Spring Security, step by step, from simple to deep.

chapter

Spring Security is a series of simple introduction and practical

The second part of the Spring Security series analyzes the authentication process

Spring Security series 3: Custom SMS Login Authentication

The fourth in the Spring Security series uses JWT for authentication

Spring Security series # 5: User authorization for backend decoupage projects

Spring Security Series 6 authorization Process Analysis

Above is the chapter of my own plan, which may or may not change, dig the hole first, then slowly fill.

Spring Security is a Security framework that provides declarative Security protection for Spring-based applications. Spring Security provides a complete Security solution, including user Authentication and Authorization. User authentication is to check whether a user has the permission to access the system. Generally, the user name and password are used for authentication, that is, login. User permissions determine which users have access to which resources.

Application scenarios

There are many reasons to use Spring Security, most of them due to the lack of security-related features in the JavaEE specification and the amount of work required to re-adapt security-related features when porting them to another set of applications. Spring Security solves these problems. It provides many useful, customizable security features.

In actual combat

Let’s take a look at a very basic security demonstration. In 2021, of course, the project will be built using SpringBoot.

To use security in SpringBoot, you simply need to introduce the corresponding starter dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
Copy the code

Add a user name and password to the configuration file:

server:
  port: 8080
spring:
  security:
    user:
      name: user
      password: 123456
Copy the code

Create a new index.html file to test logged in and unlogged access:

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>hello</title>
</head>
<body>
    <p>hello spring security</p>
</body>
</html>
Copy the code

Start the project and visit http://localhost:8080. Spring Security is already in effect. By default, spring Security intercepts all requests.

Enter your account and password to redirect to index.html.

The above project is the simplest implementation, of course, it is very imperfect and has several problems:

  1. When nothing is configured, the account and password are generated by Spring Security definitions. In the actual project, the account and password are queried from the database.
  2. The login interface is built-in. If there is anything to verify during login and which page to jump to after successful login, these must be customized.

For the first problem, we need to customize the control authentication logic, just need to implement the UserDetailsService interface.

The interface is defined as follows:

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

We implement this interface method, username is passed from the front end, we need to find the user in the database, and the user wrapped as a UserDetails object returned to security can be done.

UserDetails is also an interface that defines user-specific information:

public interface UserDetails extends Serializable {

	/** * returns the permission granted to the user, cannot be empty *@return the authorities, sorted by natural key (never <code>null</code>)
	 */
	Collection<? extends GrantedAuthority> getAuthorities();

	/** * User password *@return the password
	 */
	String getPassword(a);

	/** * User name *@return the username (never <code>null</code>)
	 */
	String getUsername(a);

	/** * Whether the user's account has expired. Expired accounts cannot be authenticated */
	boolean isAccountNonExpired(a);

	/** * Whether the user is locked or unlocked. Locked users could not be authenticated *@returnReturns true */ without locking
	boolean isAccountNonLocked(a);

	/** * Whether the user's credentials (password) have expired. Expired credentials prevent authentication */
	boolean isCredentialsNonExpired(a);

	/** * Enable or disable the user. Disabled users cannot authenticate */
	boolean isEnabled(a);

}
Copy the code

This interface has two implementation classes:

The User class simply defines the properties of the turtle, which correspond to the method in UserDetails:

public class User implements UserDetails.CredentialsContainer {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	private static final Log logger = LogFactory.getLog(User.class);

	private String password;

	private final String username;

	private final Set<GrantedAuthority> authorities;

	private final boolean accountNonExpired;

	private final boolean accountNonLocked;

	private final boolean credentialsNonExpired;

	private final boolean enabled;

	
	public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
		this(username, password, true.true.true.true, authorities);
	}

	
	public User(String username, String password, boolean enabled, boolean accountNonExpired,
			boolean credentialsNonExpired, boolean accountNonLocked,
			Collection<? extends GrantedAuthority> authorities) { Assert.isTrue(username ! =null&&!"".equals(username) && password ! =null."Cannot pass null or empty values to constructor");
		this.username = username;
		this.password = password;
		this.enabled = enabled;
		this.accountNonExpired = accountNonExpired;
		this.credentialsNonExpired = credentialsNonExpired;
		this.accountNonLocked = accountNonLocked;
		this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities)); }}/ /... omit
Copy the code

The MutableUser class is a wrapper class that contains a password attribute that wraps a change to the user password once:

class MutableUser implements MutableUserDetails {
	private String password;
	private final UserDetails delegate;
	MutableUser(UserDetails user) {
		this.delegate = user;
		this.password = user.getPassword(); }}/ /... omit
Copy the code

Most of the time, the userDetail implementation above does not meet our requirements, so we usually need to define a custom UserDetails implementation class.

Password encryption

The user is retrieved from the database, but there is no password comparison, so there must be a password resolution comparison process.

Spring Security requires you to have a PasswordEncoder instance in the container (it’s up to Spring Security to make sure the client password matches the database password; Security doesn’t have a default password parser yet). So when you customize the login logic, you must inject PaswordEncoder’s bean object into the container.

The PasswordEncoder interface is defined as follows:

public interface PasswordEncoder {

	/** * encode the original password. In general, good encoding algorithms combine a hash of SHA-1 or larger with a randomly generated salt of 8 bytes or larger */
	String encode(CharSequence rawPassword);

	/** * Validates the encoded password obtained from the store and also encodes the original password submitted. Return true if the passwords match; Otherwise, return false. The stored * password itself is not decoded. * /
	boolean matches(CharSequence rawPassword, String encodedPassword);

	/** * Return true if the encoded password needs to be encoded again to improve security, false otherwise. The default implementation always returns false */
	default boolean upgradeEncoding(String encodedPassword) {
		return false; }}Copy the code

There are many parsers built into Security:

Among them, BCryptPasswordEncoder is an official parser recommended by Spring Security. It is a concrete implementation of bcrypt strong Hash method and one-way encryption based on Hash algorithm. The encryption strength can be controlled by strength. The default value is 10. The longer the length, the higher the security.

Bcrypt has two features:

  • The HASH value is different each time
  • Calculations are very slow

Therefore, using Bcrypt for encryption, the cost of cracking the password becomes unacceptable, but the cost is the performance of the application itself, but the login behavior does not happen all the time, so it can be tolerated.

It’s also easy to use:

@SpringBootTest
@Slf4j
class SecurityApplicationTests {
    @Test
    void testPasswordEncoder(a){
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        String encode = encoder.encode("123456");
        log.info("Encoded password: {}, password correct: {}",encode,encoder.matches("123456",encode)); }}Copy the code

It is important to note that Spring Security requires that you have a PasswordEncoder instance in the container for custom login logic. So we need to write a configuration class to inject the password resolver first:

@Configuration
public class SecurityConfig {
    @Bean
    public PasswordEncoder getPasswordEncoder(a){
        return newBCryptPasswordEncoder(); }}Copy the code

Custom Login

The UserDetailsService and PasswordEncoder described above are both needed when customizing the login logic. For login, we first need to set up and complete the database prototype design.

Database table structure

To design the database table according to THE RBAC idea, here is the ER diagram:

Database script:

SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS `power`;
CREATE TABLE `power` (
  `id` int NOT NULL AUTO_INCREMENT,
  `title` varchar(32) NOT NULL.`url` varchar(64) NOT NULL,
  PRIMARY KEY (`id`))ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='rights';

DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
  `id` int NOT NULL AUTO_INCREMENT,
  `role_name` varchar(32) NOT NULL,
  PRIMARY KEY (`id`))ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

INSERT INTO `role` VALUES ('1'.'admin');
INSERT INTO `role` VALUES ('2'.'normal_user');

DROP TABLE IF EXISTS `role_power`;
CREATE TABLE `role_power` (
  `id` int NOT NULL AUTO_INCREMENT,
  `role_id` int NOT NULL.`power_id` int NOT NULL,
  PRIMARY KEY (`id`),
  KEY `role_power___fk_power_id` (`power_id`),
  KEY `role_power___fk_role_id` (`role_id`),
  CONSTRAINT `role_power___fk_power_id` FOREIGN KEY (`power_id`) REFERENCES `power` (`id`),
  CONSTRAINT `role_power___fk_role_id` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`))ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(64) NOT NULL.`password` varchar(64) NOT NULL,
  PRIMARY KEY (`id`))ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

INSERT INTO `user` VALUES ('1'.'test1'.'$2 a $10 $pjHyw9MSGC i6k546Ii / 0 ulfgtk4wyb4. 8 bsrq7yb4dy. ZpBLxOha');
INSERT INTO `user` VALUES ('2'.'test2'.'$2 a $10 $pjHyw9MSGC i6k546Ii / 0 ulfgtk4wyb4. 8 bsrq7yb4dy. ZpBLxOha');

DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int NOT NULL.`role_id` int NOT NULL.UNIQUE KEY `user_role_pk` (`id`),
  KEY `user_role___fk_role_id` (`role_id`),
  KEY `user_role___fk_user_id` (`user_id`),
  CONSTRAINT `user_role___fk_role_id` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`),
  CONSTRAINT `user_role___fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`))ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

INSERT INTO `user_role` VALUES ('1'.'1'.'1');
INSERT INTO `user_role` VALUES ('2'.'1'.'2');
INSERT INTO `user_role` VALUES ('3'.'2'.'2');
Copy the code

Configure mybatis

Add dependencies:

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.22</version>
</dependency>
Copy the code

Create a new user class and implement the UserDetails interface:

public class User implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    private List<Role> roleList;

    public Integer getId(a) {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Override
    public String getUsername(a) {
        return username;
    }

    @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 true;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roleList.stream().map(role ->
                new SimpleGrantedAuthority(role.getRoleName())).collect(Collectors.toList());
    }

    @Override
    public String getPassword(a) {
        return password;
    }

    public void setPassword(String password) {
        this.password = password; }}Copy the code

New mapper:

public interface UserDao {
	User queryByName(String name);
}
Copy the code

Corresponding XML:


      
<! DOCTYPEmapper PUBLIC "- / / mybatis.org//DTD Mapper / 3.0 / EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lin.security.dao.UserDao">
    <resultMap type="com.lin.security.entity.User" id="UserMap">
        <result property="id" column="id" jdbcType="INTEGER"/>
        <result property="username" column="username" jdbcType="VARCHAR"/>
        <result property="password" column="password" jdbcType="VARCHAR"/>
        <collection property="roleList" ofType="com.lin.security.entity.Role">
            <result property="id" column="rid" jdbcType="INTEGER"/>
            <result property="roleName" column="role_name" jdbcType="VARCHAR"/>
        </collection>
    </resultMap>
    <select id="queryByName" parameterType="java.lang.String" resultMap="UserMap">
        select u.*, r.id as rid, r.role_name
        from security.user u
        left join user_role ur on u.id = ur.user_id
        inner join role r on ur.role_id = r.id
        where username = #{name}
    </select>
 </mapper>
Copy the code

Custom login service logic, UserService interface:

@Service("userService")
@Slf4j
public class UserServiceImpl implements UserService {
	@Resource
    private UserDao userDao;
    @Override
    public UserDetails loadUserByUsername(String username) {
        User user = userDao.queryByName(username);
        if (user == null) {throw new UsernameNotFoundException("User name error");
        }
        returnuser; }}Copy the code

Here we only need to query the user, and the password verification is completed by Security.

Customize the front-end page

With the first problem solved, let’s look at the second problem.

We create three new pages, login page, login page after successful jump page, login failure page.

login.html:

<! DOCTYPEhtml>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>login</title>
</head>
<body>
    <form action="/login" method="post">
        <input type="text" name="username"/>
        <input type="password" name="password"/>
        <input type="submit" value="Submit"/>
    </form>
</body>
</html>
Copy the code

index.html:

<! DOCTYPEhtml>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>hello</title>
</head>
<body>
    <div>
        <p>hello spring security</p>
        <p>User name:<span th:text="${user.username}"></span></p>
    </div>
</body>
</html>
Copy the code

failure.html:

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>failure</title>
</head>
<body>
    <div>
        <p>The user name or password is incorrect</p>
    </div>
</body>
</html>
Copy the code

Security configuration

Configure spring security to inherit WebSecurityConfigurerAdapter class, rewrite the following three methods:

protected void configure(AuthenticationManagerBuilder auth) throws Exception {}
public void configure(WebSecurity web) throws Exception {}
protected void configure(HttpSecurity httpSecurity) throws Exception {}
Copy the code

Among them, the AuthenticationManagerBuilder used to configure the global authentication related information, is UserDetailsService and AuthenticationProvider

WebSecurity is used for global request ignore rule configuration, such as some static files, registration login page clearance.

HttpSecurity is used for specific permission control rule configuration, so we just need to override this method here.

Modify the configuration class:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //super.configure(http);
        http.formLogin()
                .loginPage("/login")  // Login page
                .successForwardUrl("/index")  // Successful login page
                .failureForwardUrl("/failure")  // Failed login page
                .and()
                // Set URL authorization
                .authorizeRequests()
                // The login page must be allowed
                .antMatchers("/login")
                .permitAll()
                // All requests other than the above must be authenticated
                .anyRequest()
                .authenticated()
                .and()
                / / close CSRF
                .csrf().disable();
    }

    @Bean
    public PasswordEncoder getPasswordEncoder(a){
        return newBCryptPasswordEncoder(); }}Copy the code

There are many other methods of HttpSecurity. Here are some common ones:

methods instructions
formLogin() Enable form authentication
loginPage() Specify the login page
successForwardUrl() Specifies the page to jump to after a successful login
failureForwardUrl() Specifies the page to jump to after a login failure
authorizeRequests() Enable access restrictions for requests using HttpServletRequest
oauth2Login() Enable oAuth2 authentication
rememberMe() openRemember that IAuthentication (using cookies)
addFilter() Add a custom filter
csrf() Enable CSRF support

Next write controller:

@Controller
public class UserController {

    @RequestMapping("/login")
    public String login(a){
        return "login";
    }

    @RequestMapping("/index")
    public String index(ModelMap modelMap){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        User principal = (User) authentication.getPrincipal();
        modelMap.put("user",principal);
        return "index";
    }
    @RequestMapping("/failure")
    public String failure(a){
        return "failure"; }}Copy the code

test

Run the project and visit http://localhost:8080. The login page is displayed. Enter the account and password to go to the login page.

Other configuration

If you need to do something else after a login succeeds or fails, the above code does not meet this requirement. You need to customize the login success/failure logic, which can be modified in the configuration file:

.successHandler((httpServletRequest, httpServletResponse, authentication) -> httpServletResponse.sendRedirect("/index"))
.failureHandler((httpServletRequest, httpServletResponse, authentication) -> httpServletResponse.sendRedirect("/failure"))
Copy the code

conclusion

The certification process

The above process is a simplified version, and the next article will cover the entire certification process in detail.

Reference:

Spring Security- Security Management Framework – Zhihu (zhihu.com)