In the past, I was mainly responsible for the user management module of the project, which involves encryption and authentication process. Encryption has been introduced in the previous article. You can read the User management module: How to Ensure user data security. Today we will talk about the technical selection and realization of authentication function. It’s not technically difficult and it’s not challenging, but it’s also a workout for a dish chicken sweet that hasn’t been certified before

Technology selection

To implement authentication, it’s easy to think of JWT or Session, but what’s the difference? What are their strengths and weaknesses? Who should be picked? Killing three even

The difference between

The main difference between session-based and JWT-BASED approaches is where the user’s state is stored. The Session is stored on the server, while the JWT is stored on the client

The certification process

Session-based authentication process
  • After the user enters the user name and password in the browser, the server verifies the password and generates a session and saves the session to the database
  • The server generates a sessionId for the user and places a cookie with the sesssionId in the user’s browser, which will be accessed on subsequent requests with this cookie information
  • The server obtains the cookie and searches the database to determine whether the current request is valid by obtaining the sessionId in the cookie
Jwt-based certification process
  • The user enters the user name and password in the browser. After password verification, the server generates a token and saves the token to the database
  • The front-end obtains the token and stores it in a cookie or local storage. Subsequent requests will be accessed with the token information
  • The server obtains the token value and checks whether the current token is valid by searching the database

The advantages and disadvantages

  • JWT is stored on the client side and requires no additional work in a distributed environment. As session is stored on the server, data sharing between multiple machines is required in a distributed environment
  • Session authentication generally requires Cookie authentication. Therefore, the browser must support Cookie authentication. Therefore, the mobile terminal cannot use the session authentication scheme
security
  • JWT payload is base64 encoded, so sensitive data cannot be stored in JWT. Session information is stored on the server side, which is more secure

If sensitive information is stored in the JWT, it can be decoded very unsecurely

performance
  • After encoding, JWT will be very long, and the limit size of cookies is generally 4K. Cookies may not fit, so JWT is generally stored in local storage. And every HTTP request the user makes in the system carries the JWT inside the Header, which may be larger than the Body. The sessionId is a very short string, so HTTP requests using JWT are much more expensive than sessions
A one-time

Statelessness is a feature of JWT, but it also causes this problem. JWT is disposable. To modify the contents, you must issue a new JWT

  • Cannot be discarded Once a JWT is issued, it remains valid until it expires and cannot be discarded. If you want to waste, a common treatment is a combination of Redis
  • Renew If JWT is used for session management, the traditional cookie renewal scheme is generally inherent in the framework. The session validity period is 30 minutes. If there is access within 30 minutes, the validity period is updated to 30 minutes. In the same way, to change the duration of a JWT, a new JWT is issued. The simplest way to do this is to refresh the JWT per request, which means that each HTTP request returns a new JWT. This method is not only violent and inelegant, but also requires JWT encryption and decryption on every request, causing performance problems. Another approach is to set the expiration time for each JWT individually in Redis and refresh the EXPIRATION time for the JWT on each access

Select JWT or Session

I vote for JWT. JWT has many disadvantages, but in distributed environment, there is no need to implement multi-machine data sharing like session. Although seesion multi-machine data sharing can solve this problem through sticky session, session sharing, session replication, persistent session, Terracoa seesion replication and other mature solutions. But JWT doesn’t require extra work, doesn’t it smell good to use JWT? And the one-time shortcomings of JWT can be remedied with REDis. Therefore, JWT was chosen for certification in the actual project

Function implementation

JWT required dependencies

<dependency> <groupId>com.auth0</groupId> <artifactId> Java -jwt</artifactId> <version>3.10.3</version> </dependency>Copy the code

JWT tools

public class JWTUtil { private static final Logger logger = LoggerFactory.getLogger(JWTUtil.class); Private static final String TOKEN_SECRET = "123456"; /** * Generate token, * * @param userTokenDTO * @return */ public static String generateToken(userTokenDTO userTokenDTO) {try Algorithm = algorithm.hmac256 (TOKEN_SECRET); Map<String, Object> header = new HashMap<>(2); header.put("Type", "Jwt"); header.put("alg", "HS256"); return JWT.create() .withHeader(header) .withClaim("token", JSONObject.toJSONString(userTokenDTO)) //.withExpiresAt(date) .sign(algorithm); } catch (Exception e) { logger.error("generate token occur error, error is:{}", e); return null; } /** * public static UserTokenDTO parseToken(String token) {Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); JWTVerifier verifier = JWT.require(algorithm).build(); DecodedJWT jwt = verifier.verify(token); String tokenInfo = jwt.getClaim("token").asString(); return JSON.parseObject(tokenInfo, UserTokenDTO.class); }}Copy the code

Description:

  • There is no expiration time in the generated token. The expiration time of the token is managed by Redis
  • UserTokenDTO does not contain sensitive information, such as the password field does not appear in the token

Redis tools

Public final class RedisServiceImpl implements RedisService {/** ** Expiration DURATION */ private final Long DURATION = 1 * 24 * 60 * 60 * 1000L; @Resource private RedisTemplate redisTemplate; private ValueOperations<String, String> valueOperations; @PostConstruct public void init() { RedisSerializer redisSerializer = new StringRedisSerializer(); redisTemplate.setKeySerializer(redisSerializer); redisTemplate.setValueSerializer(redisSerializer); redisTemplate.setHashKeySerializer(redisSerializer); redisTemplate.setHashValueSerializer(redisSerializer); valueOperations = redisTemplate.opsForValue(); } @Override public void set(String key, String value) { valueOperations.set(key, value, DURATION, TimeUnit.MILLISECONDS); log.info("key={}, value is: {} into redis cache", key, value); } @Override public String get(String key) { String redisValue = valueOperations.get(key); log.info("get from redis, value is: {}", redisValue); return redisValue; } @Override public boolean delete(String key) { boolean result = redisTemplate.delete(key); log.info("delete from redis, key is: {}", key); return result; } @Override public Long getExpireTime(String key) { return valueOperations.getOperations().getExpire(key); }}Copy the code

RedisTemplate simple encapsulation

Business implementation

Log in function
public String login(LoginUserVO loginUserVO) { //1. Check whether the user name and password are correct UserPO UserPO = usermapper.getByUsername (loginUservo.getUsername ()); if (userPO == null) { throw new UserException(ErrorCodeEnum.TNP1001001); } if (! loginUserVO.getPassword().equals(userPO.getPassword())) { throw new UserException(ErrorCodeEnum.TNP1001002); UserTokenDTO UserTokenDTO = new UserTokenDTO(); PropertiesUtil.copyProperties(userTokenDTO, loginUserVO); userTokenDTO.setId(userPO.getId()); userTokenDTO.setGmtCreate(System.currentTimeMillis()); String token = JWTUtil.generateToken(userTokenDTO); Redis redisservice.set (userpo.getid (), token); return token; }Copy the code

Description:

  • Check whether the user name and password are correct
  • If the user name and password are correct, the token is generated
  • Save the generated token to Redis
Logout function
public boolean loginOut(String id) { boolean result = redisService.delete(id); if (! redisService.delete(id)) { throw new UserException(ErrorCodeEnum.TNP1001003); } return result; }Copy the code

Delete the corresponding key

Password Update function
public String updatePassword(UpdatePasswordUserVO updatePasswordUserVO) { //1. Change passwords UserPO UserPO = UserPO. Builder (). The password (updatePasswordUserVO. GetPassword ()). The id (updatePasswordUserVO. GetId ()) .build(); UserPO user = userMapper.getById(updatePasswordUserVO.getId()); if (user == null) { throw new UserException(ErrorCodeEnum.TNP1001001); } if (userMapper.updatePassword(userPO) ! = 1) { throw new UserException(ErrorCodeEnum.TNP1001005); } / / 2. Generate a new token UserTokenDTO UserTokenDTO = UserTokenDTO. Builder (). The id (updatePasswordUserVO. GetId ()) .username(user.getUsername()) .gmtCreate(System.currentTimeMillis()).build(); String token = JWTUtil.generateToken(userTokenDTO); //3. Update token redisservice.set (user.getid (), token); return token; }Copy the code

Note: When you update a user password, you need to generate a new token and return the new token to the front end. The front end updates the token stored in the local storage and the token stored in redis at the same time. In this way, users cannot log in again and the user experience is not too bad

Other instructions
  • In the actual project, users are divided into ordinary users and administrator users. Only the administrator user has the permission to delete users. This function also involves token operation, but I am too lazy to write the demo project
  • In real projects, the password transfer is encrypted

Interceptor class

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String authToken = request.getHeader("Authorization"); String token = authToken.substring("Bearer".length() + 1).trim(); UserTokenDTO userTokenDTO = JWTUtil.parseToken(token); / / 1. Determine whether the request valid if (redisService. Get (userTokenDTO. GetId ()) = = null | |! redisService.get(userTokenDTO.getId()).equals(token)) { return false; } / / 2. Determine whether need to renew the if (redisService. GetExpireTime (userTokenDTO. GetId ()) < 1 * 60 * 30) { redisService.set(userTokenDTO.getId(), token); log.error("update token info, id is:{}, user info is:{}", userTokenDTO.getId(), token); } return true; }Copy the code

Note: The interceptor mainly does two things, one is to verify the token, the other is to determine whether the token needs to renew the token verification:

  • Check whether the token corresponding to the ID does not exist. If no, the token expires
  • If the token exists, check whether the token is consistent and ensure that only one user can perform operations at a time

Token automatic renewal: To infrequently operate Redis, update the expiration date only when the expiration date is 30 minutes away

Interceptor configuration class

@Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticateInterceptor()) .excludePathPatterns("/logout/**") .excludePathPatterns("/login/**") .addPathPatterns("/**"); } @Bean public AuthenticateInterceptor authenticateInterceptor() { return new AuthenticateInterceptor(); }}Copy the code

Write in the last

Welcome to point out any shortcomings

A “like” is going