【SpringBoot实战】实现用户名密码登录
在Java项目中,实现用户名密码登录是最基本的功能。尽管实现起来不难,但也有些细节问题,故写下此篇博客作为记录。
1.创建用户表
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之后再进行加密处理,即数据库中密码以加密的形式进行存储,可防止密码泄露。
2.创建实体类
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。
3.定义登录Dto
用户进行登录时,只需要输入用户名和密码。因此定义一个dto用于数据传输。
import lombok.Data;@Data
public class AdUserDto {private String name;private String password;
}
4.编写Controller层
@RestController
@RequestMapping("/login")
public class AdminLoginController {@Autowiredprivate AdminUserService adminUserService;@PostMapping("/in")public ResponseResult login(@RequestBody AdUserDto dto){return adminUserService.login(dto);}
}
其中,@RequestBody主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的)。
使用@RequestBody接收数据时,一般都用POST方式进行提交。
@RequestBody最多只能有一个。
5.编写Mapper层
@Mapper
public interface AdminUserMapper extends BaseMapper<AdUser> {
}
6.编写Service层
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 {@Overridepublic 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获取tokenmap.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);}}
}
7.Jwt工具类
导入依赖
<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;// 生产IDpublic 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());// 需要自动刷新TOKENif((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;}}