Spring cloud-day08 single sign on system

Posted by MrAlaska on Mon, 10 Jan 2022 02:37:27 +0100

Single sign on system

1. SCA system project

The purpose of this project is to check the permissions in the database according to the Id and whether there are users in the database according to the usename. This part is very similar to the third stage.

(1) The most important POM files are database connection dependency and mybatis plus dependency

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

    <artifactId>sca-system</artifactId>
    <!--1.Database access related-->
    <!--1.1 mysql Database driven-->
    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--1.2 mybatis plus plug-in unit-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>
        <!--Service governance related-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!--Web Service related self-contained tomcat The server-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--Current limiting depends on the need for current limiting-->
<!--        <dependency>-->
<!--            <groupId>com.alibaba.cloud</groupId>-->
<!--            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>-->
<!--        </dependency>-->
    </dependencies>
</project>

(2) Configuration file bootstrap The most important thing in the YML file is to add a data source

#Define port number
server:
  port: 8061
#Name defined in the configuration center
spring:
  application:
    name: sso-system
#Define nacos registry and nacos configuration center
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848
        file-extension: yml
#Define data source
  datasource:
    url: jdbc:mysql://localhost:3306/jt-sso?serverTimezone=Asia/Shanghai&characterEncoding=utf8
    username: root
    password: root
#mybatis-plus:
#  mapper-locations: classpath:/*.xml

logging:
  level:
    com.jt: debug

(3)pojo object - > encapsulates the field information obtained from the data

First of all, it is clear that objects are used to store data, and methods implement logic. For objects that need to encapsulate data, we will implement the serializable interface and generate a fixed UID.

package com.jt.system.pojo;

import lombok.Data;

import java.io.Serializable;

/**
 * Add @ Data to avoid writing toString getter setter method. It is worth noting that:
 * setter Method does not create a setter method for the final property
 * @Data Valid in compiler
 * idea---->jdk
 * idea---->lombok Compile
 */
@Data
//@TableName("tb_user") if the sql statement is written by itself, it is not necessary to specify the table name through @ TableName
public class User implements Serializable {
   private static final long serialVersionUID = 4831304712151465443L;
    /**
     * Object creation through reflection technology set method
     * Where is serialization used? Why fix it UID
     * Serializable Play the role of identification
     * Remember later: for objects used to store data, it is recommended to implement the serialization interface and add a serialization interface id
  
  • You can refer to String Integer ArrayList...
    • Why add UID? Because if you change the structure of the class after this, you can also deserialize successfully
    • Extract the encapsulated pojo from the database
      */
      private Long id;
      private String username;
      private String password;
      private String status;

}

1) How to generate a UID?


2) Use of lombok annotations
1. The @ data annotation will automatically generate getter, setter and toString methods for the pojo object. It should be noted that the setter method will not generate setter methods for the final modified attributes
2. The @ data annotation is only valid at compile time
3).pojo objects are created by spring through reflection technology. The set method encapsulates the information in the database onto the attributes of the object

(4) About dao layer

package com.jt.system.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jt.system.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import java.util.List;

@Mapper
/**
 * Why inherit the BaseMapper interface? A: it's OK not to write, just to use the methods in BaseMapper
 * @Mapper The annotation is provided by myBaytis and managed by the spring container to create a proxy object (the object is sqlsessiontemplate)
 * BaseMapper Single table query is supported, and objects can be used
 * Write your own implementation class?
 */
public interface UserMapper extends BaseMapper<User> {
    /**
     * Why use quotation marks?
     * @param username
     * @return
     */
    @Select("select *"+"from tb_users"+" where username = #{username}")
    User selectUserByUsername(String username);

    /**
     * According to the user id, find the license obtained by the user under the id
     * @param userId
     * @return
     */
    @Select("select distinct m.permission " +
            "from tb_user_roles ur join tb_role_menus rm on ur.role_id=rm.role_id" +
            "     join tb_menus m on rm.menu_id=m.id " +
            "where ur.user_id=#{userId}")
    List<String> selectUserPermissions(Long userId);
}

(1) Three table joint query
First, understand the relationship between tables:
One to many, JT SSO tb_ Users and JT SSO tb_ Logs to associate the logs
Many to many, add a relationship table for association
#Scheme I
select role_id from tb_user_roles where id = 1;
select menu_id from tb_role_menus where role_id in (1);
select permission from tb_menus where id in (1,2,3);

#Scheme II
select permission from tb_menus where id in
(select menu_id from tb_role_menus where role_id in
(select role_id from tb_user_roles where id = 1));

#Scheme 3: Summary: first find out what to find according to (the initial) and what to find (the final). For the rest, just associate the tables through from join on join on.
select permission from tb_menus m join tb_role_menus rm on
m.id = rm.menu_id join tb_user_roles ur on ur.role_id =rm.role_id
where ur.id =1;
(2)@Mapper
After adding this annotation, first use proxy Instance creates a proxy object of SqlSession. The proxy object executes the excuter method to access the database.
(3) There are caches and interceptors in the database
(4) Design patterns in database
Decoration mode (n cache), template mode (SqlSessionTemplate), responsibility chain mode (interceptor), factory mode, agent mode, and singleton design mode
(5)extends BaseMapper
Use mybatis plus so that you can use the objects in mybatis plus to execute sql statements. Note: mybatis plus only supports single table operations, and multi table operations are powerless
(6) Create your own mapper implementation class and hand it over to spring for management
1. Interface

package com.jt.system.dao;

import com.jt.system.pojo.User;

public interface UserDao {
    User selectUserByUserName(String username);
}

2. The implementation class needs to be managed by the spring container

import com.jt.system.dao.UserDao;
import com.jt.system.pojo.User;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class UserDaoImpl implements UserDao {
    @Autowired
    private SqlSessionTemplate sqlSessionTemplate;
    @Override
    public User selectUserByUserName(String username) {
        User user = sqlSessionTemplate.selectOne("com.jt.system.dao.UserDao.selectUserByUserName",username);
        return user;
    }
}

3. Mapping file
Map file path:

Mapping file content:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.jt.system.dao.UserDao">
    <select id="selectUserByUserName" resultType="com.jt.system.pojo.User">select * from tb_users where username = #{username} </select>
</mapper>

4. Content of configuration file:

#mybatis-plus:
#  mapper-locations: classpath:/*.xml

(7) About database connection
When connecting to the database, you must go through three handshakes and four waves of TCP, which is very time-consuming, so put the connection to the database in the connection pool.
1) Design modes in connection: single instance mode, shared element mode, bridge mode and facade mode
2) The java connection pool should be based on the DataSource specification, and the database specification is javax sql. DataSource, create a HaKariDataSource based on this specification, and create HaKariPool(CopyOnWriteArrayList storage) through this object

@SpringBootTest
//The test class must be in the same package or sub package of the main startup class
public class DataSourceTests {
    /**
     * The DataSource here is a data standard or specification. All Java connection pools need to be implemented based on this specification,
     * After we add a spring boot start JDBC dependency to our project, the system will automatically help us introduce a HikariCp connection pool
     * There is a HikariDataSource in the connection pool, and the object is based on javax sql. Datasource specification landing
     * This object is started in the SpringBoot project for automatic configuration (DataSourceAutoConfiguration)
     */
    @Autowired
    private DataSource dataSource;
    /**
     * jdk proxy object obtained by reflection
     */
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private UserDao userDao;
    @Test
    void testGetConnection() throws SQLException {
        /*
            When obtaining connections through dataSource, the first thing to obtain is the connection pool HikariPool (this pool is stored through CopyOnWriteArrayList),
            Then get the connection from the pool. This connection requires TCP connection. There are three design modes: single instance mode, shared element mode, bridge mode and facade mode
         */
        Connection conn=
                dataSource.getConnection();//Simple factory mode
        System.out.println(conn);
    }

(8) Assert
Assert.notNull(user, “user is null”);// Under the spring package, it is used for general business
Assertions.assertNotNull(user,“user is null”);// util package, general unit test

  @Test
    void testSelectUserByUsername(){
        User user =
                userMapper.selectUserByUsername("admin");
        // System.out.println(user);
        /**
       	 * When doing unit tests, you usually don't use System and use assertions
         * Test whether user is not empty here. If it is empty, an exception is thrown, and the prompt message of the exception is message
         */
        Assert.notNull(user, "user is null");//Under the spring package, it is used for general business
        Assertions.assertNotNull(user,"user is null");//util package, general unit test       

2. SCA auth project

(1) Create a pojo object to encapsulate the user object queried from the SCA system

import lombok.Data;

import java.io.Serializable;
@Data
/**
 * This user is used to encapsulate the user object returned from the controller of SCA system
 */
public class User implements Serializable {
    private static final long serialVersionUID = 3570548663999909287L;
    private Long id;
    private String username;
    private String password;
    private String status;
}

(2) Call the remote service interface through figen
1) Call figen with two annotations @ FeignClient and @ EnableFeignClients
2) Spring uses proxy Instance creates a proxy object, which is used internally to call the proxy object
3) Be sure to use @ PathVariable("username")
4) Use the parameter @ PathVariable("username") to restFull, and then call the remote server through this path.

package com.jt.auth.service;

import com.jt.auth.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import java.util.List;

@FeignClient(value = "sso-system",contextId ="remoteUserService")
/**
 * Using Fegin will create a jdk proxy object at the bottom
 * Using the Fegin object, you can automatically access the methods in SSO system
 * If you do not write contextId, the default is SSO system
 */
public interface RemoteUserService {
    //Note that the path information here should be the same as that in the controller
    @GetMapping("/user/login/{username}")
    //Note that @ PathVariable("username") must be written in fegin
    User selectUserByUsername(@PathVariable("username") String username);
    @GetMapping("/user/permission/{userId}")
    List<String> selectUserPermissions(@PathVariable("userId") Long userId);
}

(3)UserDetailsServiceImpl
There are two consistent objects that need to be specified. The return value is submitted to the Spring security authentication center for comparison (analysis), that is, the information in the database and the current userinfo
*If it can match, the login succeeds. If it does not match, the login fails
*Note that the password is not encrypted here
package com.jt.auth.service;
import org.springframework.security.core.userdetails.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;
@Service
@Slf4j
/**

  • Through fegin interface - > remote - > Database
    /
    public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private RemoteUserService remoteUserService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    /*
    *Call feign, pass in a username, give the restFull path, and then call SCA system controller according to the path
    /
    com.jt.auth.pojo.User user=
    remoteUserService.selectUserByUsername(username);
    if(user==null)
    throw new UsernameNotFoundException("user does not exist");
    //2. Used to query user permissions based on id
    List permissions=
    remoteUserService.selectUserPermissions(user.getId());
    /*
    *Prevent errors, so use logs
    /
    log.debug("permissions {}",permissions);
    //3. Encapsulate and return the query results
    /*
    *There are two consistent objects that need to be specified. The return value is submitted to the Spring security authentication center for comparison (analysis), that is, the information in the database and the current userinfo
    *If it can match, the login succeeds. If it does not match, the login fails
    *Note that the password is not encrypted here
    */
    User userInfo= new User(username,
    user.getPassword(),
    AuthorityUtils.createAuthorityList(permissions.toArray(new String[]{})));
    return userInfo;
    //Give this object to the AuthenticationManager for comparison
    }
    }
    (4) Configuration center
package com.jt.auth.config;

import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
/*
    Why use that path to access and why use post requests
 */

/**
 * When we perform the login operation, the underlying logic (understand):
 * 1)Filter(Filter)
 * 2)AuthenticationManager (Authentication manager)
 * 3)AuthenticationProvider(Authentication service processor)
 * 4)UserDetailsService(Responsible for obtaining and encapsulating user information)
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * Initialize encrypted object
     * This object provides an irreversible encryption method, which is more secure than md5
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * Define the authentication manager object, which is responsible for completing the authentication of user information,
     * That is to determine the legitimacy of user identity information and complete the authentication based on oauth2 protocol
     * This object is required when the certificate is issued, so this object is taken out and handed over to spring for management
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManagerBean()
            throws Exception {
        return super.authenticationManager();
    }

    /**Configure authentication rules
     * This method is an http request configuration method and can be configured in this method
     * 1)Which resources can be accessed without login
     * 2)Which resources need to be logged in to access
     * All resources can be accessed without any configuration.
     * */
    @Override
    protected void configure(HttpSecurity http)
            throws Exception {
        //http.authorizeRequests().antMatchers("/**").authenticated();// Indicates that all resources must be logged in before they can be accessed
        //http.authorizeRequests().antMatchers("/default.html").authenticated().anyRequest().permitAll();
        //Means except default HTML needs authentication, and others are released
        // http.authorizeRequests()
        // .anyRequest().permitAll();//2. Release access to all resources (resources can be subsequently authenticated and released based on selection)
        //super.configure(http);// By default, all requests must be authenticated. After commenting out, it means that the default page is closed and all requests are released
        //1. Disable cross domain attacks (write this first, and 403 exceptions will be reported if you don't write). If you add it, it is not disabled. If you use postman or httpclient, there will be 404 without login, because login is not configured
        //After successful login, jump to that page (before forwarding)
        //http.formLogin().successForwardUrl("/index.html").failureForwardUrl("/default.html");
        /**
         *  http.csrf().disable();Prevent third-party tools from using post for cross domain attacks
         */
        http.csrf().disable();//1. Disable cross domain attacks (browser access will not be abnormal, and third-party access will be 403 abnormal). Join is not disabled,
        http.authorizeRequests()
                .anyRequest().permitAll();//2. Release access to all resources (resources can be subsequently authenticated and released based on selection)
        //3. Customize the processing logic after login success and failure (optional)
        //If the following settings are not set, 404 will be displayed if the login is successful. This is forwarding
        /**
         * This kind of project used to separate the front and back ends will finally return a JSON
         */
        http.formLogin()//This sentence will expose a login path / login
                .successHandler(successHandler())
                .failureHandler(failureHandler());
        //http.formLogin().defaultSuccessUrl("/index.html");
        // The default of this method is redirection. Generally, the items separated from the front and back end are not required for request and forwarding. If this jump is unsuccessful, login will be returned

    }
    //Define authentication success processor
    //Return json data after successful login
    /*
    After the @ Bean annotation is added, the object will be created only once
     */
    @Bean
    public AuthenticationSuccessHandler successHandler(){
        //lambda, where authentication represents authentication information
        return (request,response,authentication)->{
            //Build a map object to encapsulate the data to respond to the client
            Map<String,Object> map=new HashMap<>();
            map.put("state",200);
            map.put("message", "login ok");
            //Convert the map object into json format string and write it to the client
            writeJsonToClient(response,map);
            /**
             * In this way, 200 is returned
             */
//           PrintWriter writer = response.getWriter();
//            writer.println("success");
//            writer.flush();
//
        };
    }
    //Define login failure processor
    @Bean
    public AuthenticationFailureHandler failureHandler(){
        return (request,response,exception)->{
            //Build a map object to encapsulate the data to respond to the client
            Map<String,Object> map=new HashMap<>();
            map.put("state",500);
            map.put("message", "login error");
            //Convert the map object into json format string and write it to the client
            writeJsonToClient(response,map);
        };

    }
    private void writeJsonToClient(
            HttpServletResponse response,
            Map<String,Object> map) throws IOException {
        //Convert the map object to json
        String json=new ObjectMapper().writeValueAsString(map);
        //Set the encoding method of response data, which is not in mysql, but in java-
        response.setCharacterEncoding("utf-8");
        //Sets the type of response data
        response.setContentType("application/json;charset=utf-8");
        //Respond data to client
        PrintWriter out=response.getWriter();
        out.println(json);
        out.flush();
    }
}

Topics: Java Back-end Spring Cloud