说明:
JWT能创建/解析token,一般由客户端上传手机号与验证码,服务器生成token,返回token给客户端,客户端使用此token访问其他需要登录的接口。
SpringBoot配置:https://blog.csdn.net/a526001650a/article/details/106687559
一、利用JWT生成/解析token,实现登录功能:
1.导入JWT依赖:
<!-- 导入JWT依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- 配置ConfigurationProperties执行器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
2.在application.yml中配置密钥与过期时间:
jwt: #jwt配置,供JWTUtils类使用
key: _yyh123= #jwt生成token时的密钥
expiration: 5000 #token 5秒超时
3.创建JWT工具类,用于生成/解析token:
//token工具类
@ConfigurationProperties(prefix = "jwt") //加载application.yml配置文件jwt节点的信息
public class JWTUtil {
private String key; //密钥,名称要与application.yml中配的一样
private long expiration; //过期时间,名称要与application.yml中配的一样
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public long getExpiration() {
return expiration;
}
public void setExpiration(long expiration) {
this.expiration = expiration;
}
//生成token
public String genToken(String userNo, String phone) {
long curTime = System.currentTimeMillis();
JwtBuilder builder = Jwts.builder()
.setId(userNo).setSubject(phone) //使用用户ID与手机号
.setIssuedAt(new Date(curTime)) //设置token创建时间
.signWith(SignatureAlgorithm.HS256, key); //进行签名,HS256方式,密钥为key
if (expiration > 0) {
builder.setExpiration(new Date(curTime + expiration)); //设置token过期时间
}
//builder.claim("key1", "value1"); //添加自定义key:value对
return builder.compact(); //生成token
}
//解析token
public Claims parser(String token) {
Claims c = Jwts.parser().setSigningKey(key) //设置签名密钥为yyh
.parseClaimsJws(token).getBody(); //解析token
String userNo = c.getId(); //获取token中的用户ID
String phone = c.getSubject(); //获取token中的手机号
Date createDate = c.getIssuedAt(); //获取token的创建时间
Date expirationDate = c.getExpiration(); //获取token的过期时间
//String value1 = (String) c.get("key1"); //获取token中的自定义key:value
return c;
}
}
4.登录生成token:
(1)在启动类中配置JWTUtil的Bean,让能被扫描到:
//申明为引导类,类名自定义
@SpringBootApplication
public class JWTApplication {
...
@Bean //类似<bean>配置,创建实例
public JWTUtil jWTUtil() {
return new JWTUtil();
}
}
(2)登录生成并返回token:
@Controller
public class LoginController {
@Autowired
private JWTUtil jWTUtil;
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public String login(String phone, String code) {
if (phone.equals("13000000000") && code.equals("123456")) { //登录成功的,生成token
String userNo = "1"; //假如从库中查出用户ID
return jWTUtil.genToken(userNo, phone); //根据userNo和手机号生成token
}
return "验证码错误";
}
}
二、使用Spring拦截器拦截其他需要登录的接口:
Zuul拦截处理(在ZuulGatewayFilter类run方法中处理):JavaEE:SpringCloud-使用Zuul网关提供统一请求入口_a526001650a的专栏-CSDN博客
1.自定义拦截器类,继承HandlerInterceptorAdapter或实现HandlerInterceptor:
@Component
public class LoginFilter extends HandlerInterceptorAdapter { //登录拦截器,或实现HandlerInterceptor
@Autowired
private JWTUtil jWTUtil;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//在接口请求处理之前校验token
String authHead = request.getHeader("Authorization");
if (StringUtils.isEmpty(authHead) !authHead.startsWith("Bearer ")) {
write(response, "10001", "没有登录");
return false; //没有登录
}
String headToken = authHead.substring(7);
try {
Claims c = jWTUtil.parser(headToken); //调用JWTUtil工具类解析token
String userNo = c.getId(); //从token中取userNo
String redisToken = redisTemplate.opsForValue().get("TOKEN_" + userNo); //根据userNo从redis中取token
if (StringUtils.isEmpty(redisToken) || !redisToken.equals(headToken)) { //将header中的token与redis中的token进行比较,一致就是合法,不一致就是不合法
write(response, "10002", "非法登录");
return false; //非法登录
}
} catch (Exception e) {//token过期时会报错
e.printStackTrace();
write(response, "10003", "token过期");
return false; //token过期
}
return true; //已登录,让通过,访问实际接口
}
private void write(HttpServletResponse response, String code, String msg) {
response.setCharacterEncoding("utf-8");
response.setContentType("text/json");
try (OutputStream out = response.getOutputStream()) {
out.write(String.format("{\"code\": \"%s\", \"msg\": \"%s\"}", code, msg).getBytes("utf-8"));
out.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.配置拦截或不拦截的接口,使用自定义拦截器,继承WebMvcConfigurationSupport或WebMvcConfigurer:
@Configuration
public class LoginFilterConfig extends WebMvcConfigurationSupport { //拦截器的一些配置,或继承WebMvcConfigurer
@Autowired
private LoginFilter loginFilter;
@Override
public void addResourceHandlers(ResourceHandlerRegistry r) {//映射静态资源
r.addResourceHandler("/**")
.addResourceLocations("classpath:/META-INF/resources/")
.addResourceLocations("file:/workspaces/images/");
}
@Override
protected void addInterceptors(InterceptorRegistry registry) {//配置拦截或不拦截的接口
registry.addInterceptor(loginFilter) //加入自定义的拦截器类
.addPathPatterns("/**") //拦截所有接口
.excludePathPatterns("/login/*"); //对登录接口放行
}
}
三、单点登录实现:
1.拦截器实现:
见上章LoginFilter
2.配置拦截器:
见上章LoginFilterConfig
3.登录/登出实现:
(1)单点登录(创建ticket):
@PostMapping("/singleLogin") //单点登录-创建ticket接口,供子系统统一登录
public String singleLogin(String phone, String code, String redirectUrl, Model model, HttpServletRequest request, HttpServletResponse response) {
if (StringUtils.isEmpty(phone) || StringUtils.isEmpty(code)) {
model.addAttribute("msg", "手机号或验证码不能为空");
return "login";
}
if (false) {
model.addAttribute("msg", "验证码错误");
return "login";
}
//1.根据手机号从库中查出用户信息
User user = new User();
//2.使用userNo为key,将用户信息缓存到redis中
redisTemplate.opsForValue().set("USER_" + user.getUserNo(), gson.toJson(user));
//3.生成全局登录标识,并缓存到redis+cookie中
Cookie c = new Cookie("TICKET", genTicket(user.getUserNo()));
c.setDomain("yyh.com");
c.setPath("/");
response.addCookie(c);
//4.生成临时登录标识(并缓存到redis中,5分钟后过期),并定向跳回登录之前的页面
return String.format("redirect:%s?tempTicket=%s", redirectUrl, genTempTicket());
}
(2)验证ticket:
@PostMapping("/verifyTicket") //单点登录验证ticket接口
@ResponseBody
public Response verifyTicket(String tempLoginId, Model model, HttpServletRequest request, HttpServletResponse response) {
//1.查询redis中是否有登录标识
String redisTempLoginId = redisTemplate.opsForValue().get("TEMP_TICKET_" + tempLoginId);
if (StringUtils.isEmpty(redisTempLoginId)) {
return new Response("10004", "登录标识异常");
}
redisTemplate.delete("TEMP_TICKET_" + tempLoginId);
//2.从cookie中取出全局登录标识,根据全局登录标识从redis中取取userNo
String ticket = getTicketByCookie(request);
//3.根据全局登录标识从redis中取取userNo
String userNo = redisTemplate.opsForValue().get("TICKET_" + ticket);
if (StringUtils.isEmpty(userNo)) {
return new Response("10004", "登录标识异常");
}
//4.根据userNo从redis中取取user信息json
String userJson = redisTemplate.opsForValue().get("USER_" + userNo);
if (StringUtils.isEmpty(userNo)) {
return new Response("10004", "登录标识异常");
}
return new Response("200", "登录标识验证通过", gson.fromJson(userJson, User.class));
}
(3)子系统二次登录:
@PostMapping("/login") //登录
public String login(String redirectUrl, Model model, HttpServletRequest request, HttpServletResponse response) {
model.addAttribute("redirectUrl", redirectUrl);
//1.获取全局ticket,能获取到,则生成临时ticket,并重定向到原网页(登录成功)
String ticket = getTicketByCookie(request);
if (verifyTicket(ticket)) {
return String.format("redirect:%s?tempTicket=%s", redirectUrl, genTempTicket()); //生成临时ticket,并重定向到原网页
}
//2.首次登录,则跳到单点登录-创建ticket
return "singleLogin";
}
(4)登出:
@PostMapping("/logout")
public String logout(String userNo, HttpServletRequest request, HttpServletResponse response) {
//1.清除redis中的ticket
redisTemplate.delete("TICKET_" + getTicketByCookie(request));
//2.清除redis中用户信息
redisTemplate.delete("USER_" + userNo);
//3.清除cookie中ticket
delTicketByCookie(response);
return "login";
}