JWT implements login authentication + Token automatic renewal scheme, this is the correct posture!
There are basically user management modules in the project, and the user management module will involve encryption and authentication processes.
Today, I will talk about the technical selection and implementation of the authentication function. It's not technically difficult and certainly not challenging, but it's also an exercise for someone who hasn't written the authentication function before.
Technical selection To achieve the authentication function, it is easy to think of JWT or session, but what is the difference between the two? Advantages and disadvantages of each? Who should Pick? Three kills
The main difference between session-based and JWT-based methods is the location where the user's state is saved. The session is saved on the server side, while the JWT is saved on the client side.
Authentication Process Session-based authentication process
The user enters the user name and password in the browser, and the server generates a session after password verification and saves it to the database server. The server generates a sessionId for the user, and places the cookie with sessionId in the user's browser, which will be stored in subsequent requests. Access the server with this cookie information to obtain the cookie, and find the database by obtaining the sessionId in the cookie to determine whether the current request is valid JWT-based authentication process
The user enters the user name and password in the browser, the server generates a token after password verification and saves it to the front end of the database to obtain the token, stores it in a cookie or local storage, and will be accessed with this token information in subsequent requests The server obtains the token value, and judges whether the current token is valid by looking up the database. Advantages and disadvantages
The JWT is stored on the client side and requires no additional work in a distributed environment. Since the session is stored on the server side, it is necessary to realize multi-machine data sharing in a distributed environment.
Session generally needs to be authenticated in combination with cookies, so the browser needs to support cookies, so the mobile terminal cannot use the security of the session authentication scheme
The payload of the JWT is base64 encoded, so no sensitive data can be stored in the JWT. The session information is stored on the server side, which is relatively more secure. If sensitive information is stored in the JWT, it can be decoded and it is very insecure.
performance
After encoding, the JWT will be very long. The limit size of the cookie is generally 4k, and the cookie may not fit, so the JWT is generally placed in the local storage. And every http request of the user in the system will carry the JWT in the Header, and the Header of the HTTP request may be larger than the Body. And sessionId is just a very short string, so the HTTP request using JWT is much more expensive than using session one-time
Statelessness is a characteristic of JWT, but also causes this problem, JWT is one-time. If you want to modify the content inside, you must issue a new JWT
Unable to discard Once a JWT is issued, it will remain valid until it expires and cannot be discarded midway. If you want to abandon it, a common treatment method is to combine redis.
Renewal If JWT is used for session management, the traditional cookie renewal scheme is generally built in the framework. The session is valid for 30 minutes. If there is access within 30 minutes, the validity period is refreshed to 30 minutes. In the same way, to change the validity time of a JWT, a new JWT must be issued. The easiest way is to refresh the JWT on every request, ie every HTTP request returns a new JWT. This method is not only violent and inelegant, but also requires JWT encryption and decryption for each request, which will bring performance problems. Another method is to set the expiration time for each JWT individually in redis, and refresh the expiration time of the JWT each time you access, select JWT or session
I vote for JWT. JWT has many shortcomings, but in a distributed environment, there is no need to additionally implement multi-machine data sharing like sessions, although seesion's multi-machine data sharing can be achieved through sticky sessions, session sharing, session replication, and persistent sessions. , terracoa implements seesion replication and other mature solutions to solve this problem.
But JWT doesn't require extra work, isn't it nice to use JWT? And the one-time disadvantage of JWT can be combined with redis to make up for it. To complement each other's strengths, in the actual project, the choice is to use JWT for authentication.
Function realization
JWT required dependencies
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
JWT utility class
public class JWTUtil {
private static final Logger logger = LoggerFactory.getLogger(JWTUtil.class);
//私钥
private static final String TOKEN_SECRET = "123456";
/**
* 生成token,自定义过期时间 毫秒
*
* @param userTokenDTO
* @return
*/
public static String generateToken(UserTokenDTO userTokenDTO) {
try {
// 私钥和加密算法
Algorithm 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;
}
}
/**
* 检验token是否正确
*
* @param token
* @return
*/
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);
}
}
illustrate:
The generated token does not have an expiration time, and the expiration time of the token is managed by redis
UserTokenDTO does not contain sensitive information, such as the password field will not appear in the token
Redis tool class
public final class RedisServiceImpl implements RedisService {
/**
* 过期时长
*/
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);
}
}
Simple package of RedisTemplate
The business implements the login function
public String login(LoginUserVO loginUserVO) {
//1.判断用户名密码是否正确
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);
}
//2.用户名密码正确生成token
UserTokenDTO userTokenDTO = new UserTokenDTO();
PropertiesUtil.copyProperties(userTokenDTO, loginUserVO);
userTokenDTO.setId(userPO.getId());
userTokenDTO.setGmtCreate(System.currentTimeMillis());
String token = JWTUtil.generateToken(userTokenDTO);
//3.存入token至redis
redisService.set(userPO.getId(), token);
return token;
}
illustrate:
Determine whether the username and password are correct and generate a token if the username and password are correct
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;
}
Just delete the corresponding key.
Update password function
public String updatePassword(UpdatePasswordUserVO updatePasswordUserVO) {
//1.修改密码
UserPO userPO = UserPO.builder().password(updatePasswordUserVO.getPassword())
.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.生成新的token
UserTokenDTO userTokenDTO = UserTokenDTO.builder()
.id(updatePasswordUserVO.getId())
.username(user.getUsername())
.gmtCreate(System.currentTimeMillis()).build();
String token = JWTUtil.generateToken(userTokenDTO);
//3.更新token
redisService.set(user.getId(), token);
return token;
}
Note: When updating the user password, it is necessary to regenerate a new token, and return the new token to the front end. The front end updates the token stored in the local storage, and at the same time updates the token stored in the redis, so that the user can be prevented from re-login. 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 right to delete users. This function also involves token operation, but I am too lazy to write the demo project in the actual project. , the password transmission is an 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.判断请求是否有效
if (redisService.get(userTokenDTO.getId()) == null
|| !redisService.get(userTokenDTO.getId()).equals(token)) {
return false;
}
//2.判断是否需要续期
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;
}
Description: The interceptor mainly does two things, one is to verify the token, and the other is to determine whether the token needs to be renew token verification:
Determine whether the token corresponding to the id does not exist. If it does not exist, the token expires. If the token exists, compare whether the tokens are consistent to ensure that only one user operates at the same time.
Token automatic renewal: In order to operate redis less frequently, the expiration time is only updated when it is only 30 minutes away from the expiration time
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();
}
}
The source code attachment has been packaged and uploaded to Baidu Cloud, you can download it yourself~
Link: https://pan.baidu.com/s/14G-bpVthImHD4eosZUNSFA?pwd=yu27
Extraction code: yu27
The Baidu cloud link is unstable and may fail at any time. Please save it as soon as possible.
If the Baidu cloud link is invalid, please leave a message and tell me, I will update it in time after I see it~
Open source address code cloud address:
https://gitee.com/ZhongBangKeJi/crmeb_java
Github address:
https://gitee.com/ZhongBangKeJi/crmeb_java
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。