文章目录
源自视频:P3.4->3.16
3.1 通用功能和配置
- 1.首先我们在service层导入解密工具AES,RSA,MD5;
- 2.自定义一个数据返回类,包含code msg data如下
- 3.自定义转换格式
- 4.自定义ConditionalException
- 5.设置全局异常GlobalException
用于对密码进行加密和解密处理
package com.imooc.bilibili.domain;
public class JsonResponse<T> {
private String code;
private String msg;
private T data;
public JsonResponse(String code,String msg){
this.code = code;
this.msg = msg;
}
public JsonResponse(T data){
this.data = data;
msg = "成功";
code = "0";
}
/**
* 成功方法,不需要返回给前端的时候
* @return
*/
public static JsonResponse<String> success (){
return new JsonResponse<>(null);
}
/**
* 根据编码返回成功
* @param code
* @param msg
* @return
*/
public static JsonResponse<String> success(String code,String msg){
return new JsonResponse<>(code,msg);
}
/**
* 默认失败的状态码和信息
* @return
*/
public static JsonResponse<String>fail(){
return new JsonResponse<>("1","失败");
}
/**
* 返回特定的状态码和提示信息
* @param code
* @param msg
* @return
*/
public static JsonResponse<String>fail(String code,String msg){
return new JsonResponse<>(code,msg);
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
- 此时我们根据返回的Json,引入fastjson包;密码解密需要用到的commons-codec依赖
- config配置类:对json的序列化进行操作
@Configuration
public class JsonHttpMessageConverterConfig {
@Bean
@Primary
public HttpMessageConverters jsonHttpMessageConverter(){
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
//配置json序列化
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
fastJsonConfig.setSerializerFeatures(
SerializerFeature.PrettyFormat,
SerializerFeature.WriteNullStringAsEmpty,
SerializerFeature.WriteNullListAsEmpty,
SerializerFeature.WriteMapNullValue,
SerializerFeature.MapSortField,
SerializerFeature.DisableCircularReferenceDetect//禁止循环引用
);
fastConverter.setFastJsonConfig(fastJsonConfig);
return new HttpMessageConverters(fastConverter);
}
}
- 自定义的ConditionalException
package com.imooc.bilibili.domain.exception;
/**
* 自定义异常
*/
public class ConditionalException extends RuntimeException{
private static final long serialVersionUID = 1L;
private String code;
private String name;
public ConditionalException(String code,String name){
super(name);
this.code = code;
}
/**
* 自定义异常返回
* @param name
*/
public ConditionalException(String name){
code = "500";
this.name = name;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
- 定义全局异常处理类
package com.imooc.bilibili.service.handle;
import com.imooc.bilibili.domain.JsonResponse;
import com.imooc.bilibili.domain.exception.ConditionalException;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CommonGlobalExceptionHandle {
@ExceptionHandler(value = Exception.class)
@ResponseBody
public JsonResponse<String>commonExceptionHandle(HttpServletRequest request, Exception e){
String errorMsg = e.getMessage();
if(e instanceof ConditionalException){
//如果是条件异常
String errorCode = ((ConditionalException) e).getCode();
return new JsonResponse<>(errorCode,errorMsg);
}else {
return new JsonResponse<>("500", errorMsg);
}
}
}
3.2用户注册与登陆
3.2.1数据库表设计
- 首先在导入sql表,我是基于mysql5.7版本开发
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`phone` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机号',
`email` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '邮箱',
`password` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '密码',
`salt` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '盐值',
`createTime` DATETIME DEFAULT NULL COMMENT '创建时间',
`updateTime` DATETIME DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
DROP TABLE IF EXISTS `t_user_info`;
CREATE TABLE `t_user_info` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`userId` BIGINT DEFAULT NULL COMMENT '用户id',
`nick` VARCHAR(100) DEFAULT NULL COMMENT '昵称',
`avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像',
`sign` TEXT COMMENT '签名',
`gender` VARCHAR(2) DEFAULT NULL COMMENT '性别:0男 1女 2未知',
`birth` VARCHAR(20) DEFAULT NULL COMMENT '生日',
`createTime` DATETIME DEFAULT NULL COMMENT '创建时间',
`updateTime` DATETIME DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户基本信息表';
- 对应我们的数据库对应字段,进行实体类的书写,我这里引入了lomback简化开发
- 建立user.java 和userInfo.java
@Data
public class User {
private Long id;
private String phone;
private String email;
private String password;
private String salt;
private Date createTime;
private Date updateTime;
private UserInfo userInfo;
}
package com.imooc.bilibili.domain;
import lombok.Data;
import java.util.Date;
@Data
public class UserInfo {
private Long id;
private Long userId;
private String nick;
private String avatar;
private String sign;
private String gender;
private String birth;
private Date createTime;
private Date updateTime;
}
3.2.2 接口的开发
开发的层级要求:dao<-service<-controller(api)<-前端页面
- 相关接口的开发:获取RSA公钥,用户注册,用户登陆
用户注册
- 获取RSA公钥
这里是我们后面用到公网映射,访问后可以得到一个RSA公钥 - 用户注册开发
@Autowired
private UserDao userDao;
/**
* 新增用户
* @param user
*/
public void addUser(User user) {
String phone = user.getPhone();
if(StringUtils.isNullOrEmpty(phone)){
//手机号为Null,抛出异常
throw new ConditionalException("手机号不能为空!");
}
User dbUser = getUserByPhone(phone);
if(dbUser!=null){
throw new ConditionalException("用户已注册!");
}
//新增操作
Date now = new Date();
String salt = String.valueOf(now.getTime());
String password = user.getPassword();
String rawPassword;
try {
rawPassword = RSAUtil.decrypt(password);
} catch (Exception e) {
throw new ConditionalException("密码解析失败");
}
String md5Password = MD5Util.sign(rawPassword, salt, "UTF-8");
user.setSalt(salt);
user.setPassword(md5Password);
user.setCreateTime(now);
userDao.addUser(user);
//添加用户信息
UserInfo userInfo = new UserInfo();
userInfo.setUserId(user.getId());
userInfo.setGender(UserConstant.GENDER_FEMALE);
userInfo.setNick(UserConstant.DEFAULT_NICK);
userInfo.setBirth(UserConstant.DEFAULT_BIRTH);
userInfo.setCreateTime(now);
userDao.addUserInfo(userInfo);
}
新增用户的模板:前端会传来一个user的json,里面包含phone password …,此时我们需要考虑是新用户还是已经存在的用户
我们基于电话或者email进行注册,首先我们根据传来的user获取phone,然后判断是否为空,然后根据得到的phone判断是否是已经存在的用户->接下来就是存数据库了,获得密码,进行解密操作因为前端会进行加密,我们解密然后再利用Md5进行加密->再加user表的信息进行set,此时user类已经有数据了,再利用userDao的addUser进行加载,同理我们的userInfo也是,其中有些信息,我们刚创建的时候是默认值,因此我们定义一个constant类,封装一些常量字段进行加载
- UserConstant接口
package com.imooc.bilibili.domain.constants;
public interface UserConstant {
public static final String GENDER_MALE = "0";
public static final String GENDER_FEMALE ="1";
public static final String GENDER_UNKNOW = "2";
public static final String DEFAULT_BIRTH = "1998-12-03";
public static final String DEFAULT_NICK = "萌新";
}
- user.xml
<insert id="addUser" parameterType="com.imooc.bilibili.domain.User" useGeneratedKeys="true" keyProperty="id">
insert into t_user
(phone,email,password,salt,createTime)
values(#{phone},#{email},#{password},#{salt},#{createTime})
</insert>
<!--进行用户的基本信息增加-->
<insert id="addUserInfo" parameterType="com.imooc.bilibili.domain.UserInfo" useGeneratedKeys="true" keyProperty="userId">
insert into t_user_info
(userId,nick,avatar,sign,gender,birth,createTime)
values(#{userId},#{nick},#{avatar},#{sign},#{gender},#{birth},#{createTime})
</insert>
3.3基于JWT的用户Token验证
- 引入JWT依赖
- 配置TokenUtils
json web token是采用了算法进行加密,所以我们需要利用JWT和Algorithm
public class TokenUtils {
public static final String ISSUER = "签名者";
/**
* 生成token
* @param userId
* @return
* @throws Exception
*/
public static String generateToken(Long userId) throws Exception {
Algorithm algorithm = Algorithm.RSA256(RSAUtil.getPublicKey(),RSAUtil.getPrivateKey());
//生成对应的token
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.add(Calendar.SECOND,30);
return JWT.create().withKeyId(String.valueOf(userId))
.withIssuer(ISSUER)
.withExpiresAt(calendar.getTime())
.sign(algorithm);
}
//token的验证
public static Long verifyToken(String token){
try {
Algorithm algorithm = Algorithm.RSA256(RSAUtil.getPublicKey(),RSAUtil.getPrivateKey());
JWTVerifier verifier = JWT.require(algorithm).build();
//进行token验证
DecodedJWT jwt = verifier.verify(token);
String userId = jwt.getKeyId();
return Long.valueOf(userId);
} catch (TokenExpiredException e) {
throw new ConditionalException("555","token过期!");
}catch (Exception e){
throw new ConditionalException("非法用户token");
}
}
- 用户的登陆验证
/**
* 用户登陆功能
* @param user
* @return
* @throws Exception
*/
@PostMapping("/user-tokens")
public JsonResponse<String>login(@RequestBody User user) throws Exception{
String token = userService.login(user);
return new JsonResponse<>(token);
}
- 登陆service层
根据传递过来的用户,我们进行账户密码核验->通过的话就返回token
/**
* 用户登陆
* @param user:包含phone id password
* @return
*/
public String login(User user) throws Exception{
String phone = user.getPhone()==null ? "":user.getPhone();
String email = user.getEmail()==null ? "":user.getEmail();
if(StringUtils.isNullOrEmpty(phone) && StringUtils.isNullOrEmpty(email)){
throw new ConditionalException("参数异常QAQ");
}
User dbUser = this.getUserByPhoneOrEmail(phone,email);
if(dbUser==null){
throw new ConditionalException("当前用户不存在");
}
String password = user.getPassword();
String rawPassword;
//进行密码解密、
try {
rawPassword = RSAUtil.decrypt(password);
} catch (Exception e) {
throw new ConditionalException("密码解密失败");
}
String salt = dbUser.getSalt();
String md5Password = MD5Util.sign(rawPassword, salt, "UTF-8");
if(!md5Password.equals(dbUser.getPassword())){
throw new ConditionalException("密码错误!");
}
//获取token
return TokenUtils.generateToken(dbUser.getId());
}
private User getUserByPhoneOrEmail(String phone, String email) {
return userDao.getUserByPhoneOrEmail(phone,email);
}
- 登陆user.xml
<!--根据手机号来查询用户-->
<select id="getUserByPhone" parameterType="java.lang.String" resultType="com.imooc.bilibili.domain.User">
select * from t_user where phone = #{phone};
</select>
<!--根据手机或邮箱来查询用户表-->
<select id="getUserByPhoneOrEmail" resultType="com.imooc.bilibili.domain.User" parameterType="java.lang.String">
select * from (select concat(phone,ifnull(email,''))as pe,u.* from t_user u) tmp
where tmp.pe like '%${phoneOrEmail}';
</select>
查询用户的信息
- controller
/**
* 查询用户基本信息
* @return
*/
@GetMapping("/users")
public JsonResponse<User> getUserInfo(){
//通过userSupport拿到userID
Long userId = userSupport.getCurrentUserId();
User user = userService.getUserByInfo(userId);
return new JsonResponse<>(user);
}
- 这里有一个UserSupport类,用来验证我们的token是否合法,其中我们的这里token是在请求头里面的
package com.imooc.bilibili.controller.support;
import com.imooc.bilibili.domain.exception.ConditionalException;
import com.imooc.bilibili.service.util.TokenUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@Component
public class UserSupport {
//获取请求头中信息
public Long getCurrentUserId(){
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String token = requestAttributes.getRequest().getHeader("token");
Long userId = TokenUtils.verifyToken(token);
if(userId < 0){
throw new ConditionalException("非法用户!");
}
return userId;
}
}
- service中来
获取User,userInfo
/**
* 获取用户信息
* @param userId
* @return
*/
public User getUserByInfo(Long userId) {
User user = userDao.getUserById(userId);
UserInfo userInfo = userDao.getUserInfoByUserId(userId);
user.setUserInfo(userInfo);
return user;
}
- user.xml 查询用户及用户信息表的sql
<!--查询用户信息-->
<select id="getUserById" resultType="com.imooc.bilibili.domain.User" parameterType="java.lang.Long">
select * from t_user where id = #{id};
</select>
<!--查询用户信息表-->
<select id="getUserInfoByUserId" resultType="com.imooc.bilibili.domain.UserInfo" parameterType="java.lang.Long">
select * from t_user_info where userId = #{userId};
</select>
修改用户表的email,phone,password字段
- controller层
@PutMapping("/users")
public JsonResponse<String> updateUsers(@RequestBody User user) throws Exception {
Long userId = userSupport.getCurrentUserId();
//前端传来的信息,我们获得id后,放进user类,在调用update
user.setId(userId);
userService.updateUsers(user);
return JsonResponse.success();
}
- service层
/**
* 用户修改
* @param user
* @return
*/
public void updateUsers(User user) throws Exception {
Long id = user.getId();
User dbUser = userDao.getUserById(id);
if(dbUser==null){
throw new ConditionalException("用户不存在");
}
if(!StringUtils.isNullOrEmpty(dbUser.getPassword())){
String rawPassword = RSAUtil.decrypt(user.getPassword());
String md5Password = MD5Util.sign(rawPassword, dbUser.getSalt(), "UTF-8");
user.setPassword(md5Password);
}
user.setUpdateTime(new Date());
userDao.updateUsers(user);
}
- dao层
<!--修改用户表-->
<update id="updateUsers" parameterType="com.imooc.bilibili.domain.User">
update t_user set
<if test="phone != null and phone != '' ">
phone = #{phone},
</if>
<if test="email != null and email != '' ">
email = #{email},
</if>
<if test="password != null and password !='' ">
password = #{password},
</if>
updateTime = #{updateTime}
where id = #{id}
</update>
修改用户基本信息、
- controller
/**
* 更新用户基本信息
* @param userInfo
* @return
*/
@PutMapping("/user-infos")
public JsonResponse<String> updateUserInfos(@RequestBody UserInfo userInfo){
Long userId = userSupport.getCurrentUserId();
userInfo.setUserId(userId);
userService.updateUserInfo(userInfo);
return JsonResponse.success();
}
- service
/**
* 用户表信息的修改
* @param userInfo
*/
public void updateUserInfo(UserInfo userInfo){
userInfo.setUpdateTime(new Date());
userDao.updateUserInfos(userInfo);
}
- dao层
<!--修改用户信息表-->
<update id="updateUserInfos" parameterType="com.imooc.bilibili.domain.UserInfo">
update t_user_info set
<if test="nick !=null and nick !='' ">
nick = #{nick},
</if>
<if test="avatar !=null">
avatar = #{avatar},
</if>
<if test="sign !=null">
sign = #{sign},
</if>
<if test="birth !=null and birth !='' ">
birth = #{birth},
</if>
<if test="gender !=null and gender !='' ">
gender = #{gender},
</if>
updateTime = #{updateTime}
where userId = #{userId};
</update>
3.4配置公网映射与使用前端调试接口
-
1、登录ngrok软件官网,并注册账号,登录官网
官网地址: ngrok -
2.进行注册与登陆
-
3.解压cmd打开ngrok
-
4.找到Key
5.成功后访问
6.6、测试是否成功将本地应用映射到公网
首先访问本地地址:
Ending第三章上半部分就在这里了
第二章链接地址:
https://blog.csdn.net/weixin_59823583/article/details/126789283?spm=1001.2014.3001.5502
补充课后习题
- 1.在使用@Autowired进行依赖注入后,idea会提示这样做不好而推荐其他注入方法,请找到这样做的原因
当使用@Autowired注解进行Bean注入的时候,Idea会提示Field injection is not recommended(Field注入),Field注入有以下缺点:
1、不能注入被final标识的对象;
2、对外部不可见,外界可以看到构造器和setter,但无法看到内部的private对象,导致无法了解具体需要的依赖有哪些
3、最重要的原因,会导致组件与IoC容器紧耦合,也就是说如果离开了IoC容器去使用这个组件,在注入依赖时就会很困
4、当需要注入的依赖过多时,如果用构造器注入就会显得庞大,但是如果用@Autowired则不会那么明显,其实这时已经违反了设计的单一职责原则
- 在用户登录接口中,我们生成了token来作为用户身份凭证,并为token设置了较短的有效期,那么假如用户退出登录或修改密码,应该如何修改token认证机制,使得用户在token有效期内仍然需要重新登录系统,请思考解决方法并尝试改造相关代码。
也就是我们上述的token会存在服务器长时间,有的时候拿着过期token也会使得能登进系统,因此需要设计一个对应的刷新机制refresh-token
-
解答
可以在用户调用登录接口成功后,在现有token返回的基础上新增一个刷新token(refresh-token),原有的token我们称之为access-token,refresh-token的有效时间相比access-token更长(一般为1周~2周),我们通常将refresh-token存储在数据库中,当用户退出登录后,前端清空用户缓存(local storage或cookie),此时我们追加操作:查询跟该access-token相关的refresh-token,将refresh-token标为无效,那么跟此refresh-token关联的access-token也将不能再继续使用。如果用户没有退出登录,继续使用系统,只需要在access-token过期的时候使用refresh-token进行刷新返回新的access-token即可。 -
代码实现逻辑
-
util类
public static String generateRefreshToken(Long userId) throws Exception{
Algorithm algorithm = Algorithm.RSA256(RSAUtil.getPublicKey(), RSAUtil.getPrivateKey());
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.add(Calendar.DAY_OF_MONTH, 7);
return JWT.create().withKeyId(String.valueOf(userId))
.withIssuer(ISSUER)
.withExpiresAt(calendar.getTime())
.sign(algorithm);
}
- service类
public Map<String, Object> loginForDts(User user) throws Exception{
String phone = user.getPhone() == null ? "" : user.getPhone();
String email = user.getEmail() == null ? "" : user.getEmail();
if(StringUtils.isNullOrEmpty(phone) && StringUtils.isNullOrEmpty(email)){
throw new ConditionException("参数异常!");
}
User dbUser = userDao.getUserByPhoneOrEmail(phone, email);
if(dbUser == null){
throw new ConditionException("当前用户不存在!");
}
String password = user.getPassword();
String rawPassword;
try{
rawPassword = RSAUtil.decrypt(password);
}catch (Exception e){
throw new ConditionException("密码解密失败!");
}
String salt = dbUser.getSalt();
String md5Password = MD5Util.sign(rawPassword, salt, "UTF-8");
if(!md5Password.equals(dbUser.getPassword())){
throw new ConditionException("密码错误!");
}
Long userId = dbUser.getId();
String accessToken = TokenUtil.generateToken(userId);
String refreshToken = TokenUtil.generateRefreshToken(userId);
//保存refresh token到数据库
userDao.deleteRefreshTokenByUserId(userId);
userDao.addRefreshToken(refreshToken, userId, new Date());
Map<String, Object> result = new HashMap<>();
result.put("accessToken", accessToken);
result.put("refreshToken", refreshToken);
return result;
}
- dao层
Integer deleteRefreshToken(@Param("refreshToken") String refreshToken,
@Param("userId") Long userId);
Integer addRefreshToken(@Param("refreshToken")String refreshToken,
@Param("userId") Long userId,
@Param("createTime") Date createTime);
- sql语句
<delete id="deleteRefreshToken">
delete from
t_refresh_token
where
refreshToken = #{refreshToken}
and userId = #{userId}
</delete>
<delete id="deleteRefreshTokenByUserId" parameterType="java.lang.Long">
delete from
t_refresh_token
where
userId = #{userId}
</delete>
<insert id="addRefreshToken">
insert into
t_refresh_token(
refreshToken,
userId,
createTime
)values(
#{refreshToken},
#{userId},
#{createTime}
)
</insert>
- controller
@PostMapping("/access-tokens")
public JsonResponse<String> refreshAccessToken(HttpServletRequest request) throws Exception {
String refreshToken = request.getHeader("refreshToken");
String accessToken = userService.refreshAccessToken(refreshToken);
return new JsonResponse<>(accessToken);
}
public String refreshAccessToken(String refreshToken) throws Exception {
RefreshTokenDetail refreshTokenDetail = userDao.getRefreshTokenDetail(refreshToken);
if(refreshTokenDetail == null){
throw new ConditionException("555","token过期!");
}
Long userId = refreshTokenDetail.getUserId();
return TokenUtil.generateToken(userId);
}