登录认证
如果不进行登录认证(对访问用户的状态进行检查),就会出现越过登录直接访问数据的bug,为了应对这种情况,我们使用JWT令牌进行验证用户登录状态。
令牌是一个字符串:
承载数据,减少访问数据库次数
防篡改
JWT令牌:
全称:JSON Web Token(https:lwt.iol) 定义了一种简洁的、自包含的格式,用于通信双方以jso数据格式安全的传输信息。 组成:
◆第一部分:Header(头),记录令牌类型、签名算法等。例如:{"alg":"HS256","type":"JWT"}
◆第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。例如:{id":"1","username'":"Tom"),注意:第二部分不要放重要信息,如(用户密码)。
◆第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、.payload,并加入指定秘钥,通过指定签名算法计算而来。
使用:
1.在pom依赖中引入坐标:
<!-- java-jwt依赖-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
2.设置令牌:
@Test
public void testGen(){
Map<String ,Object> claims = new HashMap<>();
claims.put("id",1);
claims.put("username","张三");
//生成jwt的代码
String token = JWT.create()
.withClaim("user",claims)//添加载荷
.withExpiresAt(new Date(System.currentTimeMillis()+ 1000*60*60*3))//设置令牌过期时间
.sign(Algorithm.HMAC256("itheima"));//指定算法,配置密钥
System.out.println(token);
}
3.验证令牌:
@Test
public void testParse(){
String token ="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" +
".eyJ1c2VyIjp7ImlkIjoxLCJ1c2VybmFtZSI6IuW8oOS4iSJ9LCJleHAiOjE3MjExMjk3OTR9" +
".l9HW5XNmtB9iO95sBPTIi0T5CvuSOLToPoggfAkZx1s";
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("itheima")).build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);//验证token,生成一个解析后的JWT对象
Map<String, Claim> claims = decodedJWT.getClaims();
System.out.println(claims.get("user"));
//改了任何一段都会验证失败
//令牌过期也会验证失败
}
将两段打包到JwtUtil:
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import java.util.Date;
import java.util.Map;
public class JwtUtil {
private static final String KEY = "itheima";
//接收业务数据,生成token并返回
public static String genToken(Map<String, Object> claims) {
return JWT.create()
.withClaim("claims", claims)
.withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12))
.sign(Algorithm.HMAC256(KEY));
}
//接收token,验证token,并返回业务数据
public static Map<String, Object> parseToken(String token) {
return JWT.require(Algorithm.HMAC256(KEY))
.build()
.verify(token)
.getClaim("claims")
.asMap();
}
}
在login方法中加上生成JWT令牌的流程:
@PostMapping("/login")
public Result<String> login(@Pattern(regexp = "^\\S{5,16}$") String username,@Pattern(regexp = "^\\S{5,16}$") String password){
//根据用户名查询用户
User loginUser = userService.findByUserName(username);
//判断用户是否存在
if(loginUser==null){
return Result.error("用户名错误");
}
//判断密码是否正确
if(Md5Util.getMD5String(password).equals(loginUser.getPassword())){
Map<String,Object> claims = new HashMap<>();
claims.put("id",loginUser.getId());
claims.put("username",loginUser.getUsername());
String token = JwtUtil.genToken(claims);
return Result.success(token);
}
return Result.error("密码错误");
}
验证代码:
@RestController
@RequestMapping("/article")
public class ArticleController {
@GetMapping("/list")
public Result<String> list(@RequestHeader(name = "Authorization") String token, HttpServletResponse response){
//验证token
try {
Map<String,Object> claims = JwtUtil.parseToken(token);
return Result.success("所有的文章");
} catch (Exception e) {
//http响应状态码为401
response.setStatus(401);
return Result.error(("未登录"));
}
}
}
我们可以通过拦截器来进行验证
流程:
1.写一个拦截器:
import com.atguigu.pojo.Result;
import com.atguigu.utils.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Map;
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//令牌验证
String token = request.getHeader("Authorization");
try {
Map<String,Object> claims = JwtUtil.parseToken(token);
//放行
return true;
} catch (Exception e) {
//http响应状态码为401
response.setStatus(401);
//不放行
return false;
}
}
}
2.注册拦截器,要过滤登录和注册接口
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//登录接口和注册接口不拦截
registry.addInterceptor(loginInterceptor).excludePathPatterns("/user/login","/user/register");
}
}
这样之后,业务中的验证令牌流程就可以注释掉了。
@RestController
@RequestMapping("/article")
public class ArticleController {
@GetMapping("/list")
public Result<String> list(/*@RequestHeader(name = "Authorization") String token, HttpServletResponse response*/){
//验证token
// try {
// Map<String,Object> claims = JwtUtil.parseToken(token);
// return Result.success("所有的文章");
// } catch (Exception e) {
// //http响应状态码为401
// response.setStatus(401);
// return Result.error(("未登录"));
// }
return Result.success("所有的文章");
}
}
数据返回
忽略密码
在返回对象时转为json格式会将所有属性数据列出,我们要将密码隐藏,就要在实体类的属性上加上注解:
@Data
public class User {
private Integer id;//主键ID
private String username;//用户名
@JsonIgnore//让springmvc把当前对象转换为json对象时,忽略这个字段
private String password;//密码
private String nickname;//昵称
private String email;//邮箱
private String userPic;//用户头像地址
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//更新时间
}
数据库字段格式(驼峰)
在application.yml中配置
mybatis:
configuration:
map-underscore-to-camel-case: true #开启驼峰命名和下划线命名的自动转换
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
用户信息存储
我们每次要用到当前用户信息时都要进行传参,如果有好几个类都要这个信息,每个都传太过麻烦。还有一个问题就是在同一个线程中共享数据,让多个service、Controller、dao拿到。所以我们使用ThreadLocal来保存用户信息。当登录时,拦截器顺便把用户信息存到ThreadLocal中,我们要用的时候直接调用ThreadLocal就行。步骤如下:
1.课程中提供了ThreadLocalUtil工具类,我们直接导入utils包中:
import java.util.HashMap;
import java.util.Map;
/**
* ThreadLocal 工具类
*/
@SuppressWarnings("all")
public class ThreadLocalUtil {
//提供ThreadLocal对象,
private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();
//根据键获取值
public static <T> T get(){
return (T) THREAD_LOCAL.get();
}
//存储键值对
public static void set(Object value){
THREAD_LOCAL.set(value);
}
//清除ThreadLocal 防止内存泄漏
public static void remove(){
THREAD_LOCAL.remove();
}
}
2.在拦截器中添加ThreadLocal存储,还要重写afterCompletion方法添加remove方法防止内存泄漏(THREAD_LOCAL 是全局唯一变量,如果不清除会一直驻留):
import com.atguigu.pojo.Result;
import com.atguigu.utils.JwtUtil;
import com.atguigu.utils.ThreadLocalUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Map;
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//令牌验证
String token = request.getHeader("Authorization");
try {
Map<String,Object> claims = JwtUtil.parseToken(token);
//把业务数据存储到ThreadLocal中
ThreadLocalUtil.set(claims);
//放行
return true;
} catch (Exception e) {
//http响应状态码为401
response.setStatus(401);
//不放行
return false;
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//清空ThreadLocal中的数据
ThreadLocalUtil.remove();
}
}
3.在业务中使用,用map取出数据:
@GetMapping("/userInfo")
public Result<User> userInfo(/*@RequestHeader(name = "Authorization") String token*/){
//根据用户名查询用户
// Map<String,Object> map = JwtUtil.parseToken(token);
// String username = (String) map.get("username");
Map<String,Object> map = ThreadLocalUtil.get();
String username = (String) map.get("username");
User user = userService.findByUserName(username);
return Result.success(user);
}
后续可以进一步通过redis进行优化: