Separate front and back end development - login page (back end part)

Separate front and back end development - login page (back end part)

1. Integrate the new Springboot and Mybatisplus

1.1 create project

  • Project creation

  • Add dependency

  • Modify pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com</groupId>
    <artifactId>sign09</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>sign09</name>
    <description>sign09</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>
        <!-- framework: mybatis-plus Code generation requires a template engine -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <!--mp Code generator -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.2.0</version>
        </dependency>
        <!-- hutool Tool class -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.3</version>
        </dependency>
        <!-- jwt Build tool verification tool-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <!-- shiro-redis -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis-spring-boot-starter</artifactId>
            <version>3.2.1</version>
            <exclusions>
                <exclusion>
                    <artifactId>commons-beanutils</artifactId>
                    <groupId>commons-beanutils</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>shiro-core</artifactId>
                    <groupId>org.apache.shiro</groupId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>5.3.13</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.4.0-RC2</version>
        </dependency>
        <dependency>
            <groupId>jakarta.validation</groupId>
            <artifactId>jakarta.validation-api</artifactId>
            <version>2.0.2</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
  • Modify profile
# DataSource Config
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: Lemon
mybatis-plus:
  mapper-locations: classpath*:/mapper/**Mapper.xml
server:
  port: 8081

shiro-redis:
  enable: true
  redis-manager:
    host: 127.0.0.1:6379

lemon:
  jwt:
    # Encryption key
    secret: f4e2e52034348f86b67cde581c0f9eb5
    # Valid duration of token, 7 days, in seconds
    expire: 604800
    header: Authorization

1.2 create corresponding MySQL database

DROP TABLE IF EXISTS `m_user`;

 SET character_set_client = utf8mb4 ;
CREATE TABLE `m_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(64) DEFAULT NULL,
  `avatar` varchar(255) DEFAULT NULL,
  `email` varchar(64) DEFAULT NULL,
  `password` varchar(64) DEFAULT NULL,
  `status` int(5) NOT NULL,
  `created` datetime DEFAULT NULL,
  `last_login` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `UK_USERNAME` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */; 

INSERT INTO `mybatis`.`m_user` (`id`, `username`, `avatar`, `email`, `password`, `status`, `created`, `last_login`)
VALUES ('1', 'lemon', 'https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg', NULL, '96e79218965eb72c92a549dd5a330112', '0', '2020-04-20 10:44:01', NULL);

2. Configure paging MybatisPlusConfig and generate code (dao, service, serviceImpl, etc.)

2.1 configuring paging

2.1.1 create MybatisPlusConfig class

File path: com.lemon.config

@Configuration
@EnableTransactionManagement
@MapperScan("com.lemon.mapper")
public class MybatisPlusConfig {
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        return paginationInterceptor;
    }
}

2.1.2 configuring other codes

  • Code generation:
public class CodeGenerator {

    /**
     * <p>
     * Read console content
     * </p>
     */
    public static String scanner(String tip) {
        Scanner scanner = new Scanner(System.in);
        StringBuilder help = new StringBuilder();
        help.append("Please enter" + tip + ": ");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (StringUtils.isNotEmpty(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("Please enter the correct" + tip + "!");
    }

    public static void main(String[] args) {
        // Code generator 
        AutoGenerator mpg = new AutoGenerator();

        // Global configuration
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/src/main/java");
        // gc.setOutputDir("D:\\test");
        gc.setAuthor("anonymous");
        gc.setOpen(false);
        // gc.setSwagger2(true);  Entity attribute Swagger2 annotation
        gc.setServiceName("%sService");
        mpg.setGlobalConfig(gc);

        // Data source configuration database name account password
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/mybatis?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC");
        // dsc.setSchemaName("public");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("Lemon");
        mpg.setDataSource(dsc);

        // Package configuration
        PackageConfig pc = new PackageConfig();
        pc.setModuleName(null);
        pc.setParent("com.lemon");
        mpg.setPackageInfo(pc);

        // Custom configuration
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };

        // If the template engine is freemaker
        String templatePath = "/templates/mapper.xml.ftl";
        // If the template engine is velocity
        // String templatePath = "/templates/mapper.xml.vm";

        // Custom output configuration
        List<FileOutConfig> focList = new ArrayList<>();
        // Custom configuration will be output first
        focList.add(new FileOutConfig(templatePath) {
            @Override
            public String outputFile(TableInfo tableInfo) {
                // Customize the output file name. If you set the Prefix suffix for Entity, note that the name of xml will change accordingly!
                return projectPath + "/src/main/resources/mapper/"
                        + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
            }
        });

        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);

        // Configuration template
        TemplateConfig templateConfig = new TemplateConfig();

        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);

        // Policy configuration
        StrategyConfig strategy = new StrategyConfig();
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        strategy.setEntityLombokModel(true);
        strategy.setRestControllerStyle(true);
        strategy.setInclude(scanner("Table name, separated by multiple English commas").split(","));
        strategy.setControllerMappingHyphenStyle(true);
        strategy.setTablePrefix("m_");
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }
}
  • effect

3. Phased test

@RestController
@RequestMapping("/user")
public class UserController {
    //Get the information of the user with id 1
    @GetMapping("/index")
    public  Object index(){
        
        return userService.getById(1);
    }

}

4. Unified packaging

  • Success can be indicated by code (for example, 200 indicates success and 400 indicates exception)

  • Result message

  • Result data

  • Result class (path: com.lemon.common.lang;)

@Data
public class Result implements Serializable { //serialize
    private int code; //200 is normal and 400 is abnormal
    private String msg;
    private Object data;//Return data
    //success
    public static Result succ( Object data){

        return succ(200,"Operation succeeded",data);
    }
    //success
    public static Result succ(int code,String msg,Object data){
        Result r = new Result();
        r.setCode(code);
        r.setMsg(msg);
        r.setData(data);
        return r;
    }
    //fail
    public static Result fail(String msg){

        return fail(400,msg,null);
    }
    //fail
    public static Result fail(String msg,Object data){

        return fail(400,msg,data);
    }
    //fail
    public static Result fail(int code,String msg,Object data){
        Result r = new Result();
        r.setCode(code);
        r.setMsg(msg);
        r.setData(data);
        return r;
    }

}
  • Reference test in UserController
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/index")
    public  Object index(){
        User user = userService.getById(1);
        return Result.succ(200,"Operation succeeded",user);
    }

}

5.Shiro integration jwt logic analysis

Considering that clustering and load balancing may be required later, session sharing is required. For shiro's cache and session information, we generally consider using redis to store these data. Therefore, we need to integrate not only shiro but also redis. In the open source project, we found a starter that can quickly integrate shiro redis with simple configuration. It is also recommended here.
Because what we need to do is to separate the skeleton of the project from the front and back, we generally use token or jwt as a cross domain authentication solution. Therefore, in the process of integrating shiro, we need to introduce jwt's authentication process.

  • Import Shiro redis starter package: jwt toolkit, and hutool toolkit to simplify development.
  		 <!-- shiro-redis -->
      <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis-spring-boot-starter</artifactId>
            <version>3.2.1</version>
        </dependency> 
		 <!-- hutool Tool class -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.3</version>
        </dependency>
            <!-- jwt Build tool verification tool-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

  • Create ShiroConfig

File path: com.lemon.config

@Configuration
public class ShiroConfig {
    @Autowired
    private JwtFilter jwtFilter;

    @Bean
    public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO);
        return sessionManager;
    }
    @Bean
    public DefaultWebSecurityManager securityManager(AccountRealm accountRealm,
                                                     SessionManager sessionManager,
                                                     RedisCacheManager redisCacheManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);
        securityManager.setSessionManager(sessionManager);
        securityManager.setCacheManager(redisCacheManager);
        /*
         * Close shiro's own session. See the document for details
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/**", "jwt"); // Permissions are verified mainly by annotation
        chainDefinition.addPathDefinitions(filterMap);
        return chainDefinition;
    }
    @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
                                                         ShiroFilterChainDefinition shiroFilterChainDefinition) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        Map<String, Filter> filters = new HashMap<>();
        filters.put("jwt", jwtFilter);
        shiroFilter.setFilters(filters);

        Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();

        shiroFilter.setFilterChainDefinitionMap(filterMap);

        return shiroFilter;
    }
    
}
  • Create MybatisPlusConfig

File path: com.vueblog.config

@Configuration
@EnableTransactionManagement
@MapperScan("com.lemon.mapper")
public class MybatisPlusConfig {
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        return paginationInterceptor;
    }
}
  • Create AccountRealm

File path: com.lemon.shiro

@RestController
public class AccountController {

    @Autowired
    UserService userService;

    @Autowired
    JwtUtils jwtUtils;

    @RequestMapping("/login")
    public Result login(@Validated @RequestBody LoginDto loginDto, HttpServletResponse response){
        User user = userService.getOne(new QueryWrapper<User>().eq("username", loginDto.getUsername()));
        Assert.notNull(user,"user does not exist");//Assertion interception
        //Judge whether the account and password are wrong, because they are encrypted by md5, so judge here
        if(!user.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))){
            //An exception will be thrown if the password is different
            return Result.fail("Incorrect password");
        }
        String jwt = jwtUtils.generateToken(user.getId());

        //Put the token in our header
        response.setHeader("Authorization",jwt);
        response.setHeader("Access-control-Expose-Headers","Authorization");

        return Result.succ(MapUtil.builder()
                .put("id",user.getId())
                .put("username",user.getUsername())
                .put("avatar",user.getAvatar())
                .put("email",user.getEmail()).map()

        );
    }

    //Authentication permission is required to log out
    @RequiresAuthentication
    @RequestMapping("/logout")
    public Result logout() {
        //Log out
        SecurityUtils.getSubject().logout();
        return null;
    }
}
  • Create JwtFilter
    Path: com.lemon.shiro;
@Component
public class JwtFilter extends AuthenticatingFilter {

    @Autowired
    JwtUtils jwtUtils;

    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        //Get header token
        String jwt = request.getHeader("Authorization");
        if(StringUtils.isEmpty(jwt)){
            return null;
        }
        return new JwtToken(jwt);
    }

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        //Get header token
        String jwt = request.getHeader("Authorization");
        if(StringUtils.isEmpty(jwt)){
            return true;
        }else{
            //Check jwt
            Claims claims = jwtUtils.getClaimByToken(jwt);
            //Check whether it is empty and whether the time has expired
            if(claims == null || jwtUtils.isTokenExpired(claims.getExpiration())){
                throw new ExpiredCredentialsException("token Invalid,Please log in again");

            }
            //Execute login
            return executeLogin(servletRequest,servletResponse);
        }
    }

    //Catch error rewrite method returns Result
    //Login exception logic handling
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        Throwable throwable = e.getCause() == null ? e : e.getCause();

        Result result = Result.fail(throwable.getMessage());
        //Return json
        String json = JSONUtil.toJsonStr(result);
        try {
            //Print json
            httpServletResponse.getWriter().print(json);
        }catch (IOException ioException){

        }

        return false;
    }

    /**
     * Cross domain support
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // When cross domain, we will first send an OPTIONS request. Here, we directly return to the normal state for the OPTIONS request
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

}
  • Create JwtToken

Path: com.lemon.shiro

public class JwtToken implements AuthenticationToken {
    private String token;

    public JwtToken(String jwt){
        this.token = jwt;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}
  • Create spring-devtools.properties

Path: resources.WETA-INF

restart.include.shiro-redis=/shiro-[\\w-\\.]+jar

6.Shiro logic development

  • Create the class AccountProfile to pass data

File path: com.lemon.shiro

@Data
public class AccountProfile implements Serializable {
    private Long id;

    private String username;

    private String avatar;

    private String email;

}

7. Exception handling

  • Create the GlobalExceptionHandler class

Catch global exception

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ResponseStatus(HttpStatus.UNAUTHORIZED) //Because the front and rear ends are separated, a status is returned, generally 401 has no permission
    @ExceptionHandler(value =  ShiroException.class)//Catching runtime exceptions ShiroException is the parent of most exceptions
    public Result handler(ShiroException e){
        log.error("Runtime exception:-----------------{}",e);
        return Result.fail(401,e.getMessage(),null);
    }

    /**
     * Entity verification exception
     * MethodArgumentNotValidException Capture entity verification exception
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST) //Because the front and rear ends are separated, it returns to a state
    @ExceptionHandler(value =  MethodArgumentNotValidException.class)//Catch runtime exceptions
    public Result handler(MethodArgumentNotValidException e){
        log.error("Entity capture exception:-----------------{}",e);
        BindingResult bindingException = e.getBindingResult();
        //Multiple exceptions are thrown sequentially
        ObjectError objectError = bindingException.getAllErrors().stream().findFirst().get();
        return Result.fail(objectError.getDefaultMessage());
    }

    /**
     * Assertion exception
     * @param e
     * @return
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = IllegalArgumentException.class)
    public Result handler(IllegalArgumentException e){
        log.error("Assert abnormal:------------------>{}",e);
        return Result.fail(e.getMessage());
    }


    @ResponseStatus(HttpStatus.BAD_REQUEST) //Because the front and rear ends are separated, it returns to a state
    @ExceptionHandler(value =  RuntimeException.class)//Catch runtime exceptions
    public Result handler(RuntimeException e){
        log.error("Runtime exception:-----------------{}",e);
        return Result.fail(e.getMessage());
    }
}

After using exception handling, the returned data will become concise

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (IMG oguergoc-1638273358221) (C: \ users \ lemon \ appdata \ roaming \ typora \ user images \ image-20211123151038391. PNG)]

8. Entity verification

When we submit the form data, we can use some js plug-ins such as jQuery Validate to verify the front end, while we can use hibernate validator to verify the back end.
If we use the Springboot framework as the basis, hibernate validator has been automatically integrated. (for example, verify that the login is not empty)

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("m_user")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @NotBlank(message = "Nickname cannot be empty")
    private String username;

    private String avatar;

    @NotBlank(message = "Mailbox cannot be empty")
    @Email(message = "The mailbox format is incorrect")
    private String email;

    private String password;

    private Integer status;

    private LocalDateTime created;

    private LocalDateTime lastLogin;


}
  • Write a method test in the userController class
    /**
     *
     *@RequestBody It is mainly used to receive the data in the json string passed from the front end to the back end (the data in the request body);
     * GET Method has no request body, so when using @ RequestBody to receive data,
     * The front end cannot submit data in GET mode,
     * It is submitted by POST. In the same receiving method at the back end,
     * @RequestBody It can be used simultaneously with @ RequestParam(). There can only be one @ RequestBody at most,
     * There can be multiple @ RequestParam().
     *
     * @Validated Annotation is used to check the rules filled in user, and throw an exception if they are not met
     * You can catch this exception in GlobalExceptionHandler to customize the returned data information
     */
    @PostMapping("/save")
    public  Result save(@Validated @RequestBody User user){

        return Result.succ(user);
    }

9. Cross domain issues

Because it is front-end and back-end analysis, cross domain problems can not be avoided. We directly conduct global cross domain processing in the background:
Path: com.lemon.config
Note: this configuration is configured to the controller. Before the controller, it passes through jwtFilter, so configure the cross domain problem of Filter before accessing

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }
}
  • jwtFilter for cross domain processing

Path: com.lemon.shiro

@Component
public class JwtFilter extends AuthenticatingFilter {

    @Autowired
    JwtUtils jwtUtils;

    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        //Get header token
        String jwt = request.getHeader("Authorization");
        if(StringUtils.isEmpty(jwt)){
            return null;
        }
        return new JwtToken(jwt);
    }

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        //Get header token
        String jwt = request.getHeader("Authorization");
        if(StringUtils.isEmpty(jwt)){
            return true;
        }else{
            //Check jwt
            Claims claims = jwtUtils.getClaimByToken(jwt);
            //Check whether it is empty and whether the time has expired
            if(claims == null || jwtUtils.isTokenExpired(claims.getExpiration())){
                throw new ExpiredCredentialsException("token Invalid,Please log in again");

            }
            //Execute login
            return executeLogin(servletRequest,servletResponse);
        }
    }

    //Catch error rewrite method returns Result
    //Login exception logic handling
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        Throwable throwable = e.getCause() == null ? e : e.getCause();

        Result result = Result.fail(throwable.getMessage());
        //Return json
        String json = JSONUtil.toJsonStr(result);
        try {
            //Print json
            httpServletResponse.getWriter().print(json);
        }catch (IOException ioException){

        }

        return false;
    }

    /**
     * Cross domain support
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // When cross domain, we will first send an OPTIONS request. Here, we directly return to the normal state for the OPTIONS request
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

}

So far, the basic framework has been built

10. Login interface development

  • Create LoginDto

Path: com.lemon.common.dto

@Data
public class LoginDto implements Serializable {

    @NotBlank(message = "User name cannot be empty")
    private String username;
    @NotBlank(message = "Password cannot be empty")
    private String password;
}
  • Create AccountController class

Login and exit logic

@RestController
public class AccountController {

    @Autowired
    UserService userService;

    @Autowired
    JwtUtils jwtUtils;

    @RequestMapping("/login")
    public Result login(@Validated @RequestBody LoginDto loginDto, HttpServletResponse response){
        User user = userService.getOne(new QueryWrapper<User>().eq("username", loginDto.getUsername()));
        Assert.notNull(user,"user does not exist");//Assertion interception
        //Judge whether the account and password are wrong, because they are encrypted by md5, so judge here
        if(!user.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))){
            //An exception will be thrown if the password is different
            return Result.fail("Incorrect password");
        }
        String jwt = jwtUtils.generateToken(user.getId());

        //Put the token in our header
        response.setHeader("Authorization",jwt);
        response.setHeader("Access-control-Expose-Headers","Authorization");

        return Result.succ(MapUtil.builder()
                .put("id",user.getId())
                .put("username",user.getUsername())
                .put("avatar",user.getAvatar())
                .put("email",user.getEmail()).map()

        );
    }

    //Authentication permission is required to log out
    @RequiresAuthentication
    @RequestMapping("/logout")
    public Result logout() {
        //Log out
        SecurityUtils.getSubject().logout();
        return null;
    }


}

Test:



Tags: Spring Boot Back-end

Posted on Tue, 30 Nov 2021 08:13:30 -0500 by tdeez173