study hard and make progress every day

This article has included to my lot DayDayUP:github.com/RobodLee/DayDayUP warehouse, welcome Star, more articles, please go to: directory navigation

preface

Spring Security is a powerful and highly customizable authentication and access control framework. Provides a perfect authentication mechanism and method – level authorization function. Is a very good authority management framework. At its heart is a chain of filters through which different functions pass. The purpose of this article is to use a small example to integrate Spring Security into SpringBoot. The function is to log in to the authentication server, obtain the Token, and then access the resources in the resource server.

The basic concept

  • Single sign-on (sso)

What is single sign-on? In a multi-application system, once you log in to one system, you can access its content without logging in to any other system. For example, the complex system of JINGdong is definitely not a single structure, but a microservice architecture. For example, the order function is a system, and the transaction is a system…… So I logged in when PLACING the order, do I need to log in again to pay? If so, the user experience is too poor. When I placed the order, the system found that I had not logged in and let me log in. After logging in, the system returned to me a Token, which is similar to an ID card. Then when I want to pay, I send the Token to the trading system. Then the trading system verifies the Token and knows who it is. There is no need for me to log in again.

  • JWT

The Token mentioned above is the JWT(JSON Web Token), which is a concise, URL-safe declarative specification used to transfer secure information between communication parties. A JWT is essentially a string made up of three parts, a header, a payload, and a signature. In order to intuitively see the structure of JWT, I drew a mind map:

The resulting JWT token looks like this, with three parts, separated by.

Base64UrlEncode (JWT header)+”.”+base64UrlEncode(payload)+”.”+HMACSHA256(base64UrlEncode(JWT header)+”.”+base64UrlEncode(payload), key)

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cB ab30RMHrHDcEfxjoYZgeFONFh7HgQ

  • RSA

As can be seen from the above example, JWT uses the same key “Robod666” in encryption and decryption, which will bring a disadvantage. If the hacker knows the content of the key, he can forge the Token. So for security, we can use the asymmetric encryption algorithm RSA.

RSA is based on two principles:

  • Private key encryption, can be decrypted only with a private key or public key
  • Public key encryption, private key can be decrypted

Authentication server user login function

preparation

After the introduction of the basic concept can begin to integrate, limited by space, only stick the most core code, other content please friends to find the source, address at the end of the article. First you need to get your database ready:

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
  `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT 'number',
  `ROLE_NAME` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'Role Name',
  `ROLE_DESC` varchar(60) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'Character Description'.PRIMARY KEY (`ID`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1.'ROLE_USER'.'Basic Character');
INSERT INTO `sys_role` VALUES (2.'ROLE_ADMIN'.'Super Administrator');
INSERT INTO `sys_role` VALUES (3.'ROLE_PRODUCT'.'Managed Products');
INSERT INTO `sys_role` VALUES (4.'ROLE_ORDER'.'Manage orders');

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'User name',
  `password` varchar(120) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'password',
  `status` int(1) NULL DEFAULT 1 COMMENT '1 on 0 off '.PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1.'xiaoming'.'$2a$10$CYX9OMv0yO8wR8rE19N2fOaXDJondci5uR68k2eQJm50q8ESsDMlC'.1);
INSERT INTO `sys_user` VALUES (2.'xiaoma'.'$2a$10$CYX9OMv0yO8wR8rE19N2fOaXDJondci5uR68k2eQJm50q8ESsDMlC'.1);

-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role`  (
  `UID` int(11) NOT NULL COMMENT 'User number',
  `RID` int(11) NOT NULL COMMENT 'Role Number'.PRIMARY KEY (`UID`, `RID`) USING BTREE,
  INDEX `FK_Reference_10`(`RID`) USING BTREE,
  CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `sys_user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES (1.1);
INSERT INTO `sys_user_role` VALUES (2.1);
INSERT INTO `sys_user_role` VALUES (1.3);
INSERT INTO `sys_user_role` VALUES (2.4);

SET FOREIGN_KEY_CHECKS = 1;
Copy the code

There are three tables, namely user table, role table and user-role table. The user is logged in, and the password is actually an encrypted string of “123”; Roles are used for permission control.

Then create an empty parent project, SpringSecurityDemo, and create a Module inside the parent project as the authentication service named Authentication_server. Add the necessary dependencies. (the content of the space, there is a need to obtain the source code, source address see the end of the article).

The project’s configuration file content is captured from the core section pasted below:

.....................
Public and private key positions are configured
rsa:
  key:
    pubKeyPath: C:\Users\robod\Desktop\auth_key\id_key_rsa.pub
    priKeyPath: C:\Users\robod\Desktop\auth_key\id_key_rsa
Copy the code

The final tag for the public and private keys is a custom tag, not one provided by Spring, which we will load in the RSA configuration class later.

For convenience, we can also prepare a few tool classes (content is more space, there is a need to obtain the source code, source address see the end of the article) :

  • JsonUtils: Provides json related operations;
  • JwtUtils: Token generation and token verification methods;
  • RsaUtils: generates public and private key files and reads public and private keys from files.

We can encapsulate the payload as a single object:

@Data
public class Payload<T> {
    private String id;
    private T userInfo;
    private Date expiration;
}
Copy the code

Now write a test class that calls the corresponding methods in RsaUtils to generate the public and private keys. That public key private key generated in the use of how to obtain it? To solve this problem, we need to create an RSA configuration class,

@Data
@ConfigurationProperties("rsa.key")     // Specify the key of the configuration file
public class RsaKeyProperties {

    private String pubKeyPath;

    private String priKeyPath;

    private PublicKey publicKey;
    private PrivateKey privateKey;

    @PostConstruct
    public void createKey(a) throws Exception {
        this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
        this.privateKey = RsaUtils.getPrivateKey(priKeyPath); }}Copy the code

First we specify the key of the public and private key path using the ** @configurationProperties ** annotation. Then we get the contents of the public and private key in the constructor. This allows you to call this class directly when you need a public and private key. But how to call this class without putting it in the Spring container, so add a comment to the bootstrap class:

@EnableConfigurationProperties(RsaKeyProperties.class) 
Copy the code

This means putting the RSA configuration classes into the Spring container.

The user login

Before implementing the user login function, let’s talk about login. About the login process, I read an article on the Internet and felt quite good. I posted it for my friends to see:

www.jianshu.com/p/a65f883de…

First get into UsernamePasswordAuthenticationFilter and set the permissions is null and is authorized to false, Then enter ProviderManager find support UsernamepasswordAuthenticationToken provider and the provider is called the authenticate (authentication); Then there is the implementation class of the UserDetailsService interface (that is, its own real concrete business). After checking all of these, Will callback UsernamePasswordAuthenticationFilter and set the permissions (specific business permissions) detected by authorized and set to true (because this time really checked all the all levels).

Mentioned in the passage above, a UsernamePasswordAuthenticationFilter, we started to enter is the filter attemptAuthentication () method, but this method is to obtain the form form user name password, It doesn’t fit our requirements, so we need to rewrite this method. Then after a series of turnover, into the UserDetailsService. LoadUserByUsername () method, so we in order to realize their own business logic required to implement this method. This method returns a UserDetails interface object, which you can implement if you want to return a custom object. After successful end-user authentication, Call is the parent class UsernamePasswordAuthenticationFilter AbstractAuthenticationProcessingFilter. SuccessfulAuthentication () method, We also need to rewrite this method to implement our own requirements.

So let’s implement these things 👇

@Data
public class SysUser implements UserDetails {

    private Integer id;
    private String username;
    private String password;
    private Integer status;
    private List<SysRole> roles = new ArrayList<>();	SysRole encapsulates role information, independent of login, which I'll cover later

	// There are a few more methods in UserDetails, so I won't post the code

}
Copy the code

We defined a custom SysUser class to implement the UserDetails interface, and then added a few custom fields ☝

public interface UserService extends UserDetailsService {}//-----------------------------------------------------------
@Service("userService")
public class UserServiceImpl implements UserService {.....................@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = userMapper.findByUsername(username);
        returnsysUser; }}Copy the code

In ☝, we define an interface named UserService to inherit UserDetailsService, and then implement UserService with UserServiceImpl. UserServiceImpl implements the UserDetailsService, so we can implement the loadUserByUsername() method, which simply checks the database for the corresponding SysUser. Then the specific verification process can be handed over to other filters to implement, we do not have to worry about.

AttemptAuthentication () and successfulAuthentication()** methods need to be overwritten That’s a custom filter to inherit UsernamePasswordAuthenticationFilter then rewrite this two methods 👇

public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;
    private RsaKeyProperties rsaKeyProperties;

    public JwtLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties rsaKeyProperties) {
        this.authenticationManager = authenticationManager;
        this.rsaKeyProperties = rsaKeyProperties;
    }

    // This method is used to try to authenticate the user
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            SysUser user = JSONObject.parseObject(request.getInputStream(),SysUser.class);
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                           user.getUsername(),
                           user.getPassword())
            );
        } catch (Exception e) {
            try {
                response.setContentType("application/json; charset=utf-8");
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                PrintWriter out = response.getWriter();
                Map<String, Object> map = new HashMap<>();
                map.put("code", HttpServletResponse.SC_UNAUTHORIZED);
                map.put("message"."Wrong account or password!");
                out.write(new ObjectMapper().writeValueAsString(map));
                out.flush();
                out.close();
            } catch (Exception e1) {
                e1.printStackTrace();
            }
            throw newRuntimeException(e); }}// The method to execute after success
    @Override
    public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        SysUser sysUser = new SysUser();
        sysUser.setUsername(authResult.getName());
        sysUser.setRoles((List<SysRole>) authResult.getAuthorities());
        String token = JwtUtils.generateTokenExpireInMinutes(sysUser,rsaKeyProperties.getPrivateKey(),24*60);
        response.addHeader("Authorization"."RobodToken " + token);	// Return the Token information to the user
        try {
            // If the login succeeds, a message is displayed in json format
            response.setContentType("application/json; charset=utf-8");
            response.setStatus(HttpServletResponse.SC_OK);
            PrintWriter out = response.getWriter();
            Map<String, Object> map = new HashMap<String, Object>(4);
            map.put("code", HttpServletResponse.SC_OK);
            map.put("message"."Successful landing!");
            out.write(new ObjectMapper().writeValueAsString(map));
            out.flush();
            out.close();
        } catch(Exception e1) { e1.printStackTrace(); }}}Copy the code

The logic of the code is pretty clear, so I won’t go into it.

Now, how does Spring Security know we’re going to call our own UserService and custom filters? So we need to configure, which is a core of using Spring Security — the > configuration class 👇

@Configuration
@EnableWebSecurity      // This annotation means that this class is the configuration class for Spring Security
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {.....................@Bean
    public BCryptPasswordEncoder passwordEncoder(a) {
        return new BCryptPasswordEncoder();
    }

    // Authenticate the source of the user
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
    }

    // Configure SpringSecurity information
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()  / / close CSRF
                .addFilter(new JwtLoginFilter(super.authenticationManager(),rsaKeyProperties))
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);    / / disable the session}}Copy the code

In the configuration class, you configure the source of the authentication user and add a custom filter. In this way, the login function can be realized.

The username must be “username”, the password must be “password”, and the submission mode must be “POST”.

To sum up, what do you need to do to implement the login function?

  • The authentication user implements the UserDetails interface
  • The user-derived Service implements the UserDetailsService interface and implements the loadUserByUsername() method to get data from the database
  • Implement your own filters inheritance UsernamePasswordAuthenticationFilter, rewrite attemptAuthentication () and successfulAuthentication () method to implement your own logic
  • Spring Security configuration class inherits from WebSecurityConfigurerAdapter, rewrite inside two config () method
  • If RSA asymmetric encryption is used, prepare the RSA configuration class and add annotations to the startup class to the IOC container

Verify resource server permissions

In this section, we will implement the operation of accessing and authenticating resources in the resource server. Create another module recourse_server in the parent project SpringSecirityDemo. Because we don’t need to get user information from the database right now. So you don’t need to define services and Mapper yourself. There is no need for log-in filters. The following directory structure is all you need for the resource services project.

Sysroles is used in the previous section but not explained in detail. This class encapsulates role information and implements the GrantedAuthority interface:

@Data
public class SysRole implements GrantedAuthority {

    private Integer id;
    private String roleName;
    private String roleDesc;

    /** * returns a String * if the granted permission can be treated as a String@return* /
    @JsonIgnore
    @Override
    public String getAuthority(a) {
        returnroleName; }}Copy the code

The getAuthority method is used to return roleName. RoleName is the roleName.

The client passes the Token to the resource server, which verifies the Token and retrieves the payload information. So we can customize a filter inherited from BasicAuthenticationFilter, then rewrite doFilterInternal * * * * () methods, implement your own logic.

public class JwtVerifyFilter extends BasicAuthenticationFilter {.....................@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        String header = request.getHeader("Authorization");
        // No login
        if (header == null| |! header.startsWith("RobodToken ")) {
            chain.doFilter(request, response);
            response.setContentType("application/json; charset=utf-8");
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            PrintWriter out = response.getWriter();
            Map<String, Object> map = new HashMap<String, Object>(4);
            map.put("code", HttpServletResponse.SC_FORBIDDEN);
            map.put("message"."Please log in!");
            out.write(new ObjectMapper().writeValueAsString(map));
            out.flush();
            out.close();
            return;
        }
        // Get user information from token after login
        String token = header.replace("RobodToken "."");
        SysUser sysUser = JwtUtils.getInfoFromToken(token, rsaKeyProperties.getPublicKey(), SysUser.class).getUserInfo();
        if(sysUser ! =null) {
            Authentication authResult = new UsernamePasswordAuthenticationToken
                    (sysUser.getUsername(),null,sysUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authResult); chain.doFilter(request, response); }}}Copy the code

In this code, the “Authorization” value is first obtained from the request header. If the value is not null or does not start with the specified “RobodToken”, it indicates that the Token is not set by us, or the user is not logged in, and the user is prompted to log in. Have the Token is invoked JwtUtils. GetInfoFromToken () to test and obtain the load of content. If the Authentication passes, pass the role information in the Authentication constructor and pass it to the other filters.

The private key should only be stored in the authentication server, so the resource server only needs to store the public key.

.....................
rsa:
  key:
    pubKeyPath: C:\Users\robod\Desktop\auth_key\id_key_rsa.pub
Copy the code
@Data
@ConfigurationProperties("rsa.key")     // Specify the key of the configuration file
public class RsaKeyProperties {

    private String pubKeyPath;

    private PublicKey publicKey;

    @PostConstruct
    public void createKey(a) throws Exception {
        this.publicKey = RsaUtils.getPublicKey(pubKeyPath); }}Copy the code

Next up is the configuration file for the Spring Security core 👇

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)  // enable annotation support for permission control. SecuredEnabled indicates the annotation support for permission control inside SpringSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {.....................// Configure SpringSecurity information
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()  / / close CSRF
                .authorizeRequests()
                .antMatchers("/ * *").hasAnyRole("USER") // Role information
                .anyRequest()   // Other resources
                .authenticated()    // Indicates that other resources are authenticated
                .and()
                .addFilter(new JwtVerifyFilter(super.authenticationManager(),rsaKeyProperties))
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);    / / disable the session}}Copy the code

There’s a note @ EnableGlobalMethodSecurity (securedEnabled = true), the meaning of this annotation is the annotation support open access control. Then a custom Token resolution filter was added. Finally, add annotations to the methods that require permission control at 👇

@RestController
@RequestMapping("/product")
public class ProductController {

    @Secured("ROLE_PRODUCT")
    @RequestMapping("/findAll")
    public String findAll(a) {
        return "Product list query successful"; }}Copy the code

Okay, so the findAll method needs to have the “ROLE_PRODUCT” permission to access it. Let’s test it out:

After a successful login, the Token information returned by the server is in the response header, which is copied and added to the request header we requested.

As you can see, the resource has now been successfully accessed. Try logging in to another user without permission:

The request was rejected, indicating that the permission control function is fine. To summarize the steps:

  • The class that encapsulates permission information implements the GrantedAuthority interface and implements the getAuthority() method inside
  • Implement your own Token check filter inherited from BasicAuthenticationFilter, and rewrite doFilterInternal () method, realizes own business logic
  • Write Spring Security configuration class inheritance WebSecurityConfigurerAdapter, rewrite the configure () method to add custom filters, And add @ EnableGlobalMethodSecurity (securedEnabled = true) annotations open annotation access control functions
  • If RSA asymmetric encryption is used, prepare the RSA configuration class and add annotations to the startup class to add it to the IOC container instead of just configuring the public key

conclusion

The SpringBoot integration with Spring Security ends here. The article only briefly describes the integration process, many other things are not said, such as the role of each filter, etc. Also, the separation of authentication server and resource server is possible if they are integrated together. There are many similar questions, so do your own research. Ask so that the article will not be too bloated, a lot of code is not posted, there is a need to click the link below partners can download.

Click download source

Don’t forget to like, favorites, retweet, and follow my article if it’s helpful to you. If you have any good comments, please leave them below. See you next time!