当前位置:网站首页>There's a mystery behind the little login

There's a mystery behind the little login

2022-06-29 02:45:00 Xiao Huang, who loves learning

Project introduction

Xiao Huang is learning by herself , Found a small login , There are so many hidden mysteries , Share it here , Hope to help you guys

Through the following picture , Let's take a look at the login function , What are the hidden secrets , Of course we don't use a security framework , If you want to use the security framework, you can add , You can refer to another article by Xiao Huang shiro Security framework

 Insert picture description here

Function realization

Simple function realization

Database building

Xiao Huang here uses MySQL database , We try to simulate the real environment , In fact, the only thing you can use is id、password as well as salt( salt )

 Insert picture description here

Reverse code generation

We use MyBatisPlus Operating the database , Have to say , This thing is the light of Chinese people

Just go through MyBatisPlus The code generator generates pojo layer 、dao layer 、service layer 、serviceImpl layer 、controller layer

Xiao Huang won't demonstrate here , This is not the point of this article , If you want to learn, you can read MyBatisPlus Official website

Preparation

Here is the first thing Xiao Huang learned , Return information for encapsulation

When we return an object to the browser , I used to use it directly model Send it back , And in the actual development process , The returned object should be encapsulated into a unified object , Including status code 、 Information returned

/** *  Public return class  */
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class RespBean {
    

    private long code;
    private String message;
    private Object obj;

    // Successfully returned results 
    public static RespBean success(){
    
        return new RespBean(RespBeanEnum.SUCCESS.getCode(), RespBeanEnum.SUCCESS.getMessage(), null);
    }

    // Successfully returned results 
    public static RespBean success(Object obj){
    
        return new RespBean(RespBeanEnum.SUCCESS.getCode(), RespBeanEnum.SUCCESS.getMessage(), obj);
    }

    // Failure returns result 
    public static RespBean fail(RespBeanEnum respBeanEnum){
    
        return new RespBean(respBeanEnum.getCode(),respBeanEnum.getMessage(),null);
    }

    // Failure returns result 
    public static RespBean fail(RespBeanEnum respBeanEnum,Object obj){
    
        return new RespBean(respBeanEnum.getCode(),respBeanEnum.getMessage(),obj);
    }

}
/** *  Public return enumeration class  */
@Getter
@ToString
@AllArgsConstructor
public enum RespBeanEnum {
    
    // routine 
    SUCCESS(200,"success"),
    ERROR(500," Server exception "),
    // Login module 
    LOGIN_PARAM_ERROR(500210," Parameter format error "),
    LOGIN_MOBILE_ERROR(500211," Wrong phone number format "),
    LOGIN_USER_ERROR(500212," Wrong user name or password ")
    ;

    private final Integer code;
    private final String message;
}

Here is the second thing Xiao Huang learned , Use VO Class to accept the data sent by the browser

This can help us reduce a lot of redundant code , It's like this login function , We just need to accept id( That's the phone number ) And the password

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class LoginVO {
    
    private String mobile;
    private String password;
}

MD5 encryption

Clear text transmission of passwords is prohibited in the network , If it is acquired by others, it may cause unnecessary trouble , So at the front end, we need to encrypt the password entered by the user , At the back end, we need to encrypt the user's encrypted password again

@Component
public class MD5Utils {
    

    public static String md5(String src){
    
        return DigestUtils.md5Hex(src);
    }
	// salt 
    public static final String SALT = "1a2b3c4d";
	// The front-end encryption is transmitted to the back-end ( This is just to get the encrypted data of the test , Store in database )
    public static String inputPassToFormPass(String inputPass){
    
        String str = "" + SALT.charAt(0) + SALT.charAt(2) + inputPass + SALT.charAt(5) + SALT.charAt(4);
        return md5(str);
    }
	// Back end encrypted data , The salt here is randomly generated and stored in the database when the user registers , Now let's use both front and back ends 1a2b3c4d
    public static String formPassToDbPass(String formPass, String salt){
    
        String str = "" + salt.charAt(0) + salt.charAt(2) + formPass + salt.charAt(5) + salt.charAt(4);
        return md5(str);
    }

    public static String inputPassToDbPass(String inputPass, String salt){
    
        return formPassToDbPass(inputPassToFormPass(inputPass),salt);
    }

    public static void main(String[] args) {
    
        System.out.println(inputPassToFormPass("123456")); //d3b1294a61a07da9b49b6e22b2cbd7f9
        System.out.println(formPassToDbPass("d3b1294a61a07da9b49b6e22b2cbd7f9","1a2b3c4d"));
        System.out.println(inputPassToDbPass("123456","1a2b3c4d"));
    }

}

Function realization

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    

    @Autowired
    UserMapper userMapper;

    @Autowired
    RedisTemplate redisTemplate;

    @Override
    public RespBean login(LoginVO loginVO, HttpServletRequest request, HttpServletResponse response) {
    
        // Get your mobile number and password 
        String mobile = loginVO.getMobile();
        String password = loginVO.getPassword();
        // Parameter checking 
        if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)){
    
            return RespBean.fail(RespBeanEnum.LOGIN_PARAM_ERROR);
        }
        if (!ValidatorUtils.isMobile(mobile)){
    
            return RespBean.fail(RespBeanEnum.LOGIN_MOBILE_ERROR);
        }
        // validate logon 
        User user = userMapper.selectById(mobile);
        if (user == null){
    
            throw new GlobalException(RespBeanEnum.LOGIN_USER_ERROR);
        }
        if (!MD5Utils.formPassToDbPass(password,user.getSalt()).equals(user.getPassword())){
    
            throw new GlobalException(RespBeanEnum.LOGIN_USER_ERROR);
        }

        // Store user information in cookie、session
        String ticket = UUIDUtil.uuid();
        request.getSession().setAttribute(ticket,user);
        CookieUtil.setCookie(request,response,"userTicket",ticket);
        return RespBean.success();
    }
}

Optimize one : Parameter checking

Let's take a look at the validation of parameters , The use of if Determine whether it is null ,ValidatorUtils.isMobile(mobile) Method is a regular expression check , Although the function of this verification method is feasible , But it is very unsightly for the code , At this time we have to use validation To let the system automatically verify for us

Introduce dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Using annotations

Here we just need to vo Class to achieve the verification effect

  • @NotNull: Not empty
  • @Length(min = 32): The minimum length is 32, Because by MD5 Encrypted data , All the lengths are 32
  • @IsMobile: Custom annotation , Here can speak
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class LoginVO {
    

    @NotNull
    @IsMobile
    private String mobile;

    @NotNull
    @Length(min = 32)
    private String password;

}

Use custom annotations

Custom annotations are actually very simple , Let's go in NotNull, Copy and modify it a little

@Target({
    ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
    	// Listening rules 
        validatedBy = {
     IsMobileValidation.class }
)
public @interface IsMobile {
    
    // Required 
    boolean required() default true;
	// Return message 
    String message() default " Wrong phone number format ";

    Class<?>[] groups() default {
    };

    Class<? extends Payload>[] payload() default {
    };
}


public class IsMobileValidation implements ConstraintValidator<IsMobile, String> {
    

    private boolean required = false;

    @Override
    public void initialize(IsMobile constraintAnnotation) {
    
        required = constraintAnnotation.required();
    }

    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
    
        if (required){
    
            return ValidatorUtils.isMobile(s);
        }else {
    
            if (StringUtils.isEmpty(s)){
    
                return true;
            }else {
    
                return ValidatorUtils.isMobile(s);
            }
        }
    }
}

Optimization II : exception handling

We used validation after , Errors will be found and thrown on the console , We can't tell the front end what's wrong , At this point, you need to customize exception handling

Define a global exception handling class

@Data
@NoArgsConstructor
@AllArgsConstructor
public class GlobalException extends RuntimeException{
    
    private RespBeanEnum respBeanEnum;
}

Exception handling controller

We use @RestControllerAdvice and @ExceptionHandler() Let's deal with exceptions

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    // monitor Exception It's abnormal 
    @ExceptionHandler(Exception.class)
    public RespBean exceptionHandler(Exception e){
    
        // Exceptions belong to runtime exception handling 
        if (e instanceof GlobalException){
    
            GlobalException ex = (GlobalException) e;
            return RespBean.fail(ex.getRespBeanEnum());
        }else if (e instanceof BindException){
     //validation The type of exception thrown is BindException
            // Abnormal mobile number format 
            BindException ex = (BindException) e;
            RespBean respBean = RespBean.fail(RespBeanEnum.LOGIN_MOBILE_ERROR);
            respBean.setMessage(" Parameter format error :" + ex.getBindingResult().getAllErrors().get(0).getDefaultMessage());// Get exception return information 
            return respBean;
        }
        return RespBean.fail(RespBeanEnum.ERROR);
    }
}

Modify the business layer code

    @Override
    public RespBean login(LoginVO loginVO, HttpServletRequest request, HttpServletResponse response) {
    
        // Get your mobile number and password 
        String mobile = loginVO.getMobile();
        String password = loginVO.getPassword();
        // Parameter checking 
// if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)){
    
// return RespBean.fail(RespBeanEnum.LOGIN_PARAM_ERROR);
// }
// if (!ValidatorUtils.isMobile(mobile)){
    
// return RespBean.fail(RespBeanEnum.LOGIN_MOBILE_ERROR);
// }
        // validate logon 
        User user = userMapper.selectById(mobile);
        if (user == null){
    
            throw new GlobalException(RespBeanEnum.LOGIN_USER_ERROR);
        }
        if (!MD5Utils.formPassToDbPass(password,user.getSalt()).equals(user.getPassword())){
    
            throw new GlobalException(RespBeanEnum.LOGIN_USER_ERROR);
        }

        // Store user information in cookie、session
        String ticket = UUIDUtil.uuid();
        request.getSession().setAttribute(ticket,user);
        CookieUtil.setCookie(request,response,"userTicket",ticket);
        return RespBean.success();
    }

Optimize three : Distributed session problem

When we deploy the project on a server , The above code can completely realize the login function , Sometimes we need to deal with highly concurrent requests , Spread the pressure of the server , The project will be deployed on multiple servers , But at the same time, the problem is that each request is randomly assigned to different servers to process . We go through redis To solve this problem

Solution 1 : Use session-data-redis

Common use redis Only the first two dependencies need to be introduced , To solve the above problem, we need to introduce a third dependency .

After the introduction, you will find , Don't do anything , He automatically saved our data to redis in , However, the data stored here is stored in binary form , It's not convenient to read

<!-- spring-data-redis rely on  -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--redis Connection pool -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.11.1</version>
</dependency>
<!--session-data-redis-->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

Yes redis To configure

spring:
  # redis  To configure 
  redis:
    host: 121.40.45.37
    port: 6379
    lettuce:
      pool:
        #  maximum connection 
        max-active: 8
        #  Maximum number of free connections 
        max-idle: 200
        #  Connection blocking timeout 
        max-wait: 10000
        #  Minimum number of idle connections 
        min-idle: 5
    database: 0
    timeout: 10000
    password: Hkx123

Solution 2 : Use redis Store user login information , Instead of session

Here we mainly solve the binary problem ,redis Configuration class , We set up serialization , Let him JSON Format data to store information

@Configuration
public class RedisConfig {
    
    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
    
        RedisTemplate<String,Object> redisTemplate = new RedisTemplate();
        // Set serialization , Otherwise, it will be stored in binary form 
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        // Injection connection factory 
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }
}

Modify the business layer

    @Override
    public RespBean login(LoginVO loginVO, HttpServletRequest request, HttpServletResponse response) {
    
        // Get your mobile number and password 
        String mobile = loginVO.getMobile();
        String password = loginVO.getPassword();
        // Parameter checking 
// if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)){
    
// return RespBean.fail(RespBeanEnum.LOGIN_PARAM_ERROR);
// }
// if (!ValidatorUtils.isMobile(mobile)){
    
// return RespBean.fail(RespBeanEnum.LOGIN_MOBILE_ERROR);
// }
        // validate logon 
        User user = userMapper.selectById(mobile);
        if (user == null){
    
            throw new GlobalException(RespBeanEnum.LOGIN_USER_ERROR);
        }
        if (!MD5Utils.formPassToDbPass(password,user.getSalt()).equals(user.getPassword())){
    
            throw new GlobalException(RespBeanEnum.LOGIN_USER_ERROR);
        }

        // Store user information in cookie、session
        String ticket = UUIDUtil.uuid();
        // If not distributed , The following statements can be used to resolve user login information 
// request.getSession().setAttribute(ticket,user);
        // Store user information in redis in 
        redisTemplate.opsForValue().set("user:" + ticket,user);
        CookieUtil.setCookie(request,response,"userTicket",ticket);
        return RespBean.success();
    }

Optimize four : Code tedious problem

We will jump to other pages after login , Our requirement is that you need login information to access other pages

as follows , Need to ticket, according to tiket Get user login information

@Controller
@RequestMapping("/goods")
public class GoodsController {
    
    @Autowired
    IUserService userService;

    @GetMapping("/toList")
    public String toList(Model model,String ticket){
    
        if (StringUtils.isEmpty(ticket)){
    
            return "login";
        }
// User user = (User) session.getAttribute(ticket);
        User user = userService.getUserByRedis(ticket);
        if (user == null){
    
            return "login";
        }
        model.addAttribute("user",user);
        return "goodsList";
    }
}

In this way, all our requests must be added with these statements before we can continue to execute , It seems a little cumbersome

Use MVC solve the problem

Our idea is , Before receiving this request , First get user object , And judge that it is not empty , And then the user The object is passed in as a parameter

Modify the above code

    @GetMapping("/toList")
    public String toList(Model model,User user){
    
// if (StringUtils.isEmpty(ticket)){
    
// return "login";
// }
 User user = (User) session.getAttribute(ticket);
// User user = userService.getUserByRedis(ticket);
// if (user == null){
    
// return "login";
// }
        model.addAttribute("user",user);
        return "goodsList";
    }

MVC Configuration class

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
    
    @Autowired
    UserArgumentResolver userArgumentResolver;

    @Override//HandlerMethodArgumentResolver This is the custom parameter parser we need to use 
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
    
        resolvers.add(userArgumentResolver);
    }
}

Custom parameter resolver UserArgumentResolver

@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
    

    @Autowired
    IUserService userService;

    //supportsParameter return true Will execute resolveArgument
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
    
        Class<?> clazz = parameter.getParameterType();
        return clazz == User.class;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer
            , NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
        String ticket = CookieUtil.getCookieValue(request, "userTicket");

        if (StringUtils.isEmpty(ticket)){
    
            return null;
        }

        return userService.getUserByRedis(ticket,request,response);
    }
}

summary

Above, we have completed the optimization of the login function , Xiao Huang is just a beginner , If there are better suggestions , Leave a comment in the comments section !!!

原网站

版权声明
本文为[Xiao Huang, who loves learning]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/02/202202161128554257.html