在前后端分离的场景中,很多时候会采用token的方案进行登录校验。
登录成功时,后端会根据一些用户信息生成一个token字符串返回给前端。
前端会存储这个token。以后前端发起请求时如果有token就会把token放在请求头中发送给后端。
后端接口就可以获取请求头中的token信息进行解析,如果解析不成功说明token超时了或者不是正确的token,相当于是未登录状态。
如果解析成功,说明前端是已经登录过的。
Token生成方案-JWT
本案例采用目前企业中运用比较多的JWT来生成token。
使用时先引入相关依赖
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.0version>
dependency>
然后可以使用下面的工具类来生成和解析token
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
/**
* JWT工具类
*/
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "sangeng";
/**
* 创建token
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if(ttlMillis==null){
ttlMillis=JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
SecretKey secretKey = generalKey();
JwtBuilder builder = Jwts.builder()
.setId(id) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("sg") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);// 设置过期时间
return builder.compact();
}
/**
* 生成加密后的秘钥 secretKey
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
登录接口实现
响应类
import com.fasterxml.jackson.annotation.JsonInclude;
/**
* @Author
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T> {
/**
* 状态码
*/
private Integer code;
/**
* 提示信息,如果有错误时,前端可以获取该字段进行提示
*/
private String msg;
/**
* 查询到的结果数据,
*/
private T data;
public ResponseResult(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public ResponseResult(Integer code, T data) {
this.code = code;
this.data = data;
}
public Integer getCode() {
return code;
}
public void setCode(Integer 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;
}
public ResponseResult(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
}
数据准备
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) DEFAULT NULL,
`password` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
/*Data for the table `sys_user` */
insert into `sys_user`(`id`,`username`,`password`) values (1,'root','root'),(2,'sangeng','caotang');
实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SystemUser {
private Integer id;
private String username;
private String password;
}
SystemUserController
import com.springboot.entity.ResponseResult;
import com.springboot.entity.SystemUser;
import com.springboot.service.SystemUserService;
import com.springboot.utils.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/sys_user")
public class SystemUserController {
@Autowired
private SystemUserService userService;
@PostMapping("/login")
public ResponseResult login(@RequestBody SystemUser user) {
//校验用户名密码是否正确
SystemUser loginUser = userService.login(user);
Map<String, Object> map;
if (loginUser != null) {
//如果正确 生成token返回
map = new HashMap<>();
String token = JwtUtil.createJWT(UUID.randomUUID().toString(), String.valueOf(loginUser.getId()), null);
map.put("token", token);
} else {
//如果不正确 给出相应的提示
return new ResponseResult(300, "用户名或密码错误,请重新登录");
}
return new ResponseResult(200, "登录成功", map);
}
}
Service
public interface SystemUserService {
public SystemUser login(SystemUser user);
}
@Service
public class SystemUserServcieImpl implements SystemUserService {
@Autowired
private SystemUserMapper systemUserMapper;
@Override
public SystemUser login(SystemUser user) {
SystemUser loginUser = systemUserMapper.login(user);
return loginUser;
}
}
Mapper
@Mapper
@Repository
public interface SystemUserMapper {
SystemUser login(SystemUser user);
}
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.springboot.mapper.SystemUserMapper">
<select id="login" resultType="com.springboot.entity.SystemUser">
select * from sys_user where username = #{username} and password = #{password}
select>
mapper>
登录页面(可用接口工具测试)
如果我们想在多个Handler方法执行之前或者之后都进行一些处理,甚至某些情况下需要拦截掉,不让Handler方法执行。那么可以使用SpringMVC为我们提供的拦截器。
①创建类实现HandlerInterceptor接口
public class LoginInterceptor implements HandlerInterceptor {
}
②实现方法
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取请求头中的token
String token = request.getHeader("token");
//判断token是否为空,如果为空也代表未登录 提醒重新登录(401)
if(!StringUtils.hasText(token)){
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
//解析token看看是否成功
try {
Claims claims = JwtUtil.parseJWT(token);
String subject = claims.getSubject();
System.out.println(subject);
} catch (Exception e) {
e.printStackTrace();
//如果解析过程中没有出现异常说明是登录状态
//如果出现了异常,说明未登录,提醒重新登录(401)
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
return true;
}
}
③配置拦截器
@Configuration
public class LoginConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)//添加拦截器
.addPathPatterns("/**") //配置拦截路径
.excludePathPatterns("/sys_user/login");//配置排除路径
}
}
①创建类加上@ControllerAdvice注解进行标识
@ControllerAdvice
public class MyControllerAdvice {
}
②定义异常处理方法
定义异常处理方法,使用**@ExceptionHandler**标识可以处理的异常。
@ControllerAdvice
public class MyControllerAdvice {
@ExceptionHandler(RuntimeException.class)
@ResponseBody
public ResponseResult handlerException(Exception e){
//获取异常信息,存放如ResponseResult的msg属性
String message = e.getMessage();
ResponseResult result = new ResponseResult(300,message);
//把ResponseResult作为返回值返回,要求到时候转换成json存入响应体中
return result;
}
}
我们之前在web阶段我们经常要使用到request对象,response,session对象等。我们也可以通过SpringMVC获取到这些对象。(不过在MVC中我们很少获取这些对象,因为有更简便的方式,避免了我们使用这些原生对象相对繁琐的API。)
我们只需要在方法上添加对应类型的参数即可,但是注意数据类型不要写错了,SpringMVC会把我们需要的对象传给我们的形参。
@RestController
public class TestController {
@RequestMapping("/getRequestAndResponse")
public ResponseResult getRequestAndResponse(HttpServletRequest request, HttpServletResponse response, HttpSession session){
System.out.println(request);
return new ResponseResult(200,"成功");
}
}
如果我们想实现像获取请求体中的数据那样,在Handler方法的参数上增加一个@RepuestBody注解就可以获取到对应的数据的话。
可以使用HandlerMethodArgumentResolver来实现自定义的参数解析。
①定义用来标识的注解
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUserId {
}
②创建类实现HandlerMethodArgumentResolver接口并重写其中的方法
@Component
public class UserIdArgumentResolver implements HandlerMethodArgumentResolver {
//判断方法参数使用能使用当前的参数解析器进行解析
@Override
public boolean supportsParameter(MethodParameter parameter) {
//如果方法参数有加上CurrentUserId注解,就能把被我们的解析器解析
return parameter.hasParameterAnnotation(CurrentUserId.class);
}
//进行参数解析的方法,可以在方法中获取对应的数据,然后把数据作为返回值返回。方法的返回值就会赋值给对应的方法参数
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
//获取请求头中的token
String token = webRequest.getHeader("token");
if(StringUtils.hasText(token)){
//解析token,获取userId
Claims claims = JwtUtil.parseJWT(token);
String userId = claims.getSubject();
//返回结果
return userId;
}
return null;
}
}
③配置参数解析器
@Configuration
public class ArgumentResolverConfig implements WebMvcConfigurer {
@Autowired
private UserIdArgumentResolver userIdArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(userIdArgumentResolver);
}
}
④测试
在需要获取UserId的方法中增加对应的方法参数然后使用@CurrentUserId进行标识即可获取到数据
@RestController
@RequestMapping("/user")
//@CrossOrigin
public class UserController {
@Autowired
private UserServcie userServcie;
@RequestMapping("/findAll")
public ResponseResult findAll(@CurrentUserId String userId) throws Exception {
System.out.println(userId);
//调用service查询数据 ,进行返回s
List<User> users = userServcie.findAll();
return new ResponseResult(200,users);
}
}