当前位置:网站首页>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

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 )

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 !!!
边栏推荐
猜你喜欢
随机推荐
信息学奥赛一本通 1361:产生数(Produce) | 洛谷 P1037 [NOIP2002 普及组] 产生数
sql连续登录问题
PHP的system函数
Analytic hierarchy process (AHP)
18. `bs對象.節點名.next_sibling` 獲取兄弟節點
哪个证券公司最大最安全 哪家券商服务好
音响是如何把微弱声音放大呢
Prepare for the Blue Bridge Cup - double pointer, BFS
学习太极创客 — MQTT 第二章(九)本章测试
Pytoch Learning Series: Introduction
Quelques tests pour compléter l'environnement wasm
短视频平台常见SQL面试题,你学会了吗?
Have you learned the common SQL interview questions on the short video platform?
PMP Business Analysis Overview
fsockopen函数的应用
PHP XML expat parser
99乘法表
Exec function of PHP
[linear algebra] 1.2 total permutation and commutation
String substitution









