在Java项目中,实现用户名密码登录是最基本的功能。尽管实现起来不难,但也有些细节问题,故写下此篇博客作为记录。
CREATE TABLE `ad_user` (
`id` int unsigned NOT NULL COMMENT '主键',
`name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '登录用户名',
`password` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '登录密码',
`salt` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '盐',
`nickname` varchar(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '昵称',
`image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '头像',
`phone` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机号',
`status` tinyint unsigned DEFAULT NULL COMMENT '状态\r\n 0 暂时不可用\r\n 1 永久不可用\r\n 9 正常可用',
`email` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '邮箱',
`login_time` datetime DEFAULT NULL COMMENT '最后一次登录时间',
`created_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='管理员用户信息表'
其中,密码经过加salt之后再进行加密处理,即数据库中密码以加密的形式进行存储,可防止密码泄露。
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
@TableName("ad_user")
public class AdUser implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 密码、通信等加密盐
*/
@TableField("salt")
private String salt;
/**
* 用户名
*/
@TableField("name")
private String name;
/**
* 密码,md5加密
*/
@TableField("password")
private String password;
/**
* 昵称
*/
@TableField("nickname")
private String nickname;
/**
* 手机号
*/
@TableField("phone")
private String phone;
/**
* 头像
*/
@TableField("image")
private String image;
/**
* 状态
0 暂时不可用
1 永久不可用
9 正常可用
*/
@TableField("status")
private Integer status;
/**
* 邮箱
*/
@TableField("email")
private String email;
/**
* 创建时间
*/
@TableField("created_time")
private Date createdTime;
/**
* 最后一次登录时间
*/
@TableField("login_time")
private Date loginTime;
}
其中,实体类实现了Serializable接口,并定义了serialVersionUID变量。
Serializable是一个对象序列化的接口,可将实体对象进行序列化。
如果我们没有声明一个serialVersionUID变量,接口会默认生成一个serialVersionUID,默认的serialVersinUID对于class的细节非常敏感,反序列化时可能会导致InvalidClassException异常,因此建议我们自定义一个serialVersionUID。
用户进行登录时,只需要输入用户名和密码。因此定义一个dto用于数据传输。
import lombok.Data;
@Data
public class AdUserDto {
private String name;
private String password;
}
@RestController
@RequestMapping("/login")
public class AdminLoginController {
@Autowired
private AdminUserService adminUserService;
@PostMapping("/in")
public ResponseResult login(@RequestBody AdUserDto dto){
return adminUserService.login(dto);
}
}
其中,@RequestBody主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的)。
使用@RequestBody接收数据时,一般都用POST方式进行提交。
@RequestBody最多只能有一个。
@Mapper
public interface AdminUserMapper extends BaseMapper<AdUser> {
}
service层使用了mybatis-plus,在Service接口需要继承IService,在serviceImpl实现类需要继承ServiceImpl。
public interface AdminUserService extends IService<AdUser> {
public ResponseResult login(AdUserDto dto);
}
业务层实现用户名密码登录逻辑如下:
@Service
public class AdminUserServiceImpl extends ServiceImpl<AdminUserMapper, AdUser> implements AdminUserService {
@Override
public ResponseResult login(AdUserDto dto) {
// 1.检查参数是否为空
if (StringUtils.isBlank(dto.getName()) || StringUtils.isBlank(dto.getPassword())){
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID, "用户名或密码为空");
}
// 2.查询用户是否存在
AdUser adUser = getOne(Wrappers.<AdUser>lambdaQuery().eq(AdUser::getName, dto.getName()));
if (adUser == null){
return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST);
}
// 3.对比密码是否正确
String salt = adUser.getSalt();
String password = dto.getPassword();
// 采用md5码加密
password = DigestUtils.md5DigestAsHex((password + salt).getBytes());
if (password.equals(adUser.getPassword())){
// 4.返回数据
Map<String, Object> map = new HashMap<>();
// 根据id获取token
map.put("token", AppJwtUtil.getToken(adUser.getId().longValue()));
// 将salt和密码清空
adUser.setSalt("");
adUser.setPassword("");
map.put("user", adUser);
return ResponseResult.okResult(map);
}else {
return ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_PASSWORD_ERROR);
}
}
}
导入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
Jwt工具类中包含了token的生成及校验方法。
关于Jwt的刷新机制,建议参考如下文章:SpringBoot+JWT登录校验,以及JWT刷新机制
import io.jsonwebtoken.*;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;
public class AppJwtUtil {
// TOKEN的有效期一小时(S)
private static final int TOKEN_TIME_OUT = 3_600;
// 加密私钥
private static final String TOKEN_ENCRY_KEY = "MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY";
// 最小刷新间隔(S)
private static final int REFRESH_TIME = 300;
// 生产ID
public static String getToken(Long id){
Map<String, Object> claimMaps = new HashMap<>();
claimMaps.put("id",id);
long currentTime = System.currentTimeMillis();
return Jwts.builder()
.setId(UUID.randomUUID().toString())
.setIssuedAt(new Date(currentTime)) //签发时间
.setSubject("system") //说明
.setIssuer("daybreak") //签发者信息
.setAudience("app") //接收用户
.compressWith(CompressionCodecs.GZIP) //数据压缩方式
.signWith(SignatureAlgorithm.HS512, generalKey()) //加密方式
.setExpiration(new Date(currentTime + TOKEN_TIME_OUT * 1000)) //过期时间戳
.addClaims(claimMaps) //claim信息
.compact();
}
/**
* 获取token中的claims信息
* @param token
* @return
*/
private static Jws<Claims> getJws(String token) {
return Jwts.parser()
.setSigningKey(generalKey())
.parseClaimsJws(token);
}
/**
* 获取payload body信息
* @param token
* @return
*/
public static Claims getClaimsBody(String token) {
try {
return getJws(token).getBody();
}catch (ExpiredJwtException e){
return null;
}
}
/**
* 获取hearder body信息
* @param token
* @return
*/
public static JwsHeader getHeaderBody(String token) {
return getJws(token).getHeader();
}
/**
* 是否过期
* @param claims
* @return -1:有效,0:有效,1:过期,2:过期
*/
public static int verifyToken(Claims claims) {
if(claims==null){
return 1;
}
try {
claims.getExpiration().before(new Date());
// 需要自动刷新TOKEN
if((claims.getExpiration().getTime()-System.currentTimeMillis())>REFRESH_TIME*1000){
return -1;
}else {
return 0;
}
} catch (ExpiredJwtException ex) {
return 1;
}catch (Exception e){
return 2;
}
}
/**
* 由字符串生成加密key
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getEncoder().encode(TOKEN_ENCRY_KEY.getBytes());
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
}