目录:
1 关于JWT的一些介绍
1.1 什么是JWT?
1.2 JWT是干嘛的 以及 JWT和SessionToken的区别
1.3 JWT的工作原理(以最简单的模型为例)
2 JWT在SpringBoot的应用
2.1 导入maven依赖
2.2 修改旧版 WebSecurityConfigure
2.3 创建Filter过滤器
2.4 创建util工具类:JwtTokenUtil
2.5 创建Result结果集
2.6 创建LoginController
2.7 新增一个entity(保留了之前的entity)
2.8 修改yml文件
2.9 认证测试
3 浅谈JWT在开发应用中的优缺点
3.1 JWT的优势
3.2 JWT的不足
1 关于JWT的一些介绍
1.1 什么是JWT?
JWT:JSON Web Tokens
- JWT-JSON Web Token是目前最流行的跨域认证解决方案,是一种认证授权机制
- JWT 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准。JWT 的声明一般被用来在身份提供者和服务器提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。
- 可以用HMAC算法或者RSA的公钥密钥对JWT进行数字签名。
1.2 JWT是干嘛的 以及 JWT和SessionToken的区别
在这个问题之前,首先明白JWT为了解决什么问题
我们知道HTTP是无状态协议
在发送请求的时候,服务器只需要给我们返回一个页面就行了。每次访问是独立的,下一次访问时,它就不记得我了。
但是当我们需要让服务器记住我们的时候,比如以下场景:
- 我们在登录一些页面时,根据我们的用户级别,显示不同的资源和页面。
- 用户在访问一些本应该无权访问的页面或者文件时
- 用户需要保留自己的一些数据,但是存在客户端
- .....
这些时候,相比于存在客户端(容易篡改、容易丢失),我们更需要服务器通过session域来存储一些数据,比如(浏览量、权限和页面资源)让网页能够正常运转。
现在我们可以问JWT是干嘛的?
JWT是为了解决上述众多问题的一个:
认证和授权 ( Authentication & Authorization)
其中常见的授权策略是Sessiontoken和JWT
区别就在于Session是有状态,存在于服务器,通过保存在客户端的SessionId对用户进行唯一性识别
而JWT是无状态的,用户第一次访问时会收到一个token,
token根据用户的信息映射出来的,相当于一份具有用户签名的令牌.
存储在cookie里或者localStorage里,第二次访问时带上token,服务器能够通过密钥进行解析用户名,实现用户唯一性识别、认证。
因此可以看出,Session token是 引用型数据,只有用户的sessionId
JWT是 包含用户名本身的一类传输数据
1.3 JWT的工作原理(以最简单的模型为例)
流程就像1.2 中讲述的那样:
①用户输入用户名(username)+密码(password)登录,服务端查询数据库的当前用户,进行密码校验,成功后,会根据用户信息,返回给客户端一个JWT(包含Header、Payload、签名Signature)
②客户端将token保存在本地(通常是localStorage/cookie)
③用户访问一个受保护的资源或者路由的时候,请求头的Authorization字段中使用Bearer模式添加JWT
④服务器收到含有JWT的请求时,先通过密钥解析出用户的username,然后数据库查询用户的权限,然后根据权限给用户放行或者拒绝访问。
链接:
(这位作者有点口音)
【应用安全】JWT授权到底是关于什么_哔哩哔哩_bilibili
session、token、JWT的一文详细介绍_傲娇味的草莓的博客-CSDN博客_jwt session token
2 JWT在SpringBoot的应用
本文代码是基于Security的基础上进行的改进和增强
这是我的上一篇:
SpringBoot中的Security简单入门实现_敬叫唤的博客-CSDN博客
2.1 导入maven依赖
只增加了
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- 版本号-->
<properties>
<java.version>1.8</java.version>
<mysql.version>8.0.30</mysql.version>
<mybatis.springboot.version>1.3.2</mybatis.springboot.version>
</properties>
<dependencies>
<!-- springboot启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- test测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mybatis和mysql驱动-->
<!-- mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.springboot.version}</version>
</dependency>
<!-- mysqsl-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- 分页-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.3</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.3.1</version>
</dependency>
<!-- 导入JPA依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>net.minidev</groupId>
<artifactId>json-smart</artifactId>
</dependency>
<!-- redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- AOP 织入 ,这里用于 日志收集-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
<scope>compile</scope>
</dependency>
<!-- Security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
2.2 修改旧版 WebSecurityConfigure
(注释的代码是之前的Security实现方式)
主要增加了:
JwtAuthenticationTokenFilte
RestAuthenticationEntryPoint
RestfulAccessDeniedHandler
同时取消了security自带的login接口,需要使用自己的 /login接口
取消了在配置类中添加加密方式passwordEncoder
import com.wanxi.springboot1018.filter.JwtAuthenticationTokenFilter;
import com.wanxi.springboot1018.result.RestAuthenticationEntryPoint;
import com.wanxi.springboot1018.result.RestfulAccessDeniedHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
// 这个有了以下任何一个注解都可以不用这个注解
// @Configuration
// 这个表示启用Web安全的注解,如果你已经是是一个web 项目,不需要使用此注解,
// @EnableWebSecurity //Springboot的自动配置机制WebSecurityEnablerConfiguration已经引入了该注解
//开启这个来判断用户对某个控制层的方法是否具有访问权限(见ProductController的@PreAuthorize)
// 这个注解很重要,如果没有这个注解,那么Controller里的方法将不受约束,只要登录成功就能访问。
@EnableGlobalMethodSecurity(prePostEnabled = true) //至关重要的注解,缺失会导致验证不起效
@EnableWebSecurity
public class WebSecurityConfigure extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
//过滤器
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
//入口
@Autowired
private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
//访问拒绝处理器
@Autowired
private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
// 参数: HttpSecurity http
//**http.authorizeRequests()**
// 下添加了多个匹配器,每个匹配器用来控制不同的URL接受不同的用户访问。
// 简单讲,http.authorizeRequests()就是在进行请求的权限配置。
@Override
protected void configure(HttpSecurity http) throws Exception {
//第二步:我们用我们自己的数据库数据来完成权限验证
// 开启跨域和关闭csrf保护
http.cors().and().csrf().disable()
.sessionManagement()//允许配置会话管理
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)//Spring Security不会创建HttpSession,也不会使用它获取SecurityContext
.and()
.authorizeRequests()
.antMatchers("/index","/login").permitAll()// index 放行
.antMatchers(HttpMethod.OPTIONS).permitAll()//options 方法的请求放行
.anyRequest().authenticated()// 其它请求要认证
.and()
// 这一步,告诉Security 框架,我们要用自己的UserDetailsService实现类
// 来传递UserDetails对象给框架,框架会把这些信息生成Authorization对象使用
.userDetailsService(userDetailsService);
// 过滤前,我们使用jwt进行过滤
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// http.authorizeRequests()
// .antMatchers("/index").permitAll()//放行
// .anyRequest().authenticated()
// .and()
// .formLogin()//访问security自带的login接口
// .and()
// // 这一步,告诉Security 框架,我们要用自己的UserDetailsService实现类
// // 来传递UserDetails对象给框架,框架会把这些信息生成Authorization对象使用
// .userDetailsService(userDetailsService);
//添加自定义未授权和未登录结果返回
http.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint);
}
// //这里配置密码为 BCrypt 加密方式,这样创建用户时,会对密码进行加密。而不是明文存储。
// @Bean
// public PasswordEncoder passwordEncoder() {
// return new BCryptPasswordEncoder();
// }
}
2.3 创建Filter过滤器
这个过滤器很重要,截取请求中的token代码都在这里实现。
import com.wanxi.springboot1018.utils.JwtTokenUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* JWT登录授权过滤器
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.tokenHeader}")//Authorization
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;//bearer
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
//从 header 中获取 Authorization
String authHeader = request.getHeader(this.tokenHeader); //tokenHead :'bearer'
if (authHeader == null) { // 如果 authHeader为空,意味着没有token,放行,自动被权限拦截。
chain.doFilter(request, response);
return;
}
boolean b1 = StringUtils.startsWithIgnoreCase(authHeader,this.tokenHead);
if (!b1) {
chain.doFilter(request, response);// 如果 没有以 bearer 开头, 放行
return;
}
//截取 bearer 后面的字符串 并且 trim: 两端去空(获取token)
String authToken = authHeader.substring(this.tokenHead.length()).trim();// The part after "Bearer "
String username = jwtTokenUtil.getUserNameFromToken(authToken);
LOGGER.info("checking username:{}", username);
// 用户名不为空 并且SecurityContextHolder.getContext() 存储 权限的容器中没有相关权限则继续
boolean isNotAuthentication = SecurityContextHolder.getContext().getAuthentication() == null;
if (username != null && isNotAuthentication) {
//从数据库读取用户信息
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
//校验token
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
// 一个实现的带用户名和密码以及权限的Authentication(spring 自带的类)
UsernamePasswordAuthenticationToken authentication = null;
authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
// 从HttpServletRequest 对象,创建一个WebAuthenticationDetails对象
WebAuthenticationDetails details = new WebAuthenticationDetailsSource().buildDetails(request);
//设置details
authentication.setDetails(details);
LOGGER.info("authenticated user:{}", username);
//存入本线程的安全容器 在访问接口拿到返回值后 要去主动清除 权限,避免干扰其他的线程
//SecurityContextHolder会把authentication放入到session里,供后面使用
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
}
2.4 创建util工具类:JwtTokenUtil
package com.wanxi.springboot1018.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import sun.util.logging.PlatformLogger;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JwtToken生成的工具类
* JWT token的格式:header.payload.signature
* header的格式(算法、token的类型):
* {"alg": "HS512","typ": "JWT"}
* payload的格式(用户名、创建时间、生成时间):
* {"sub":"wang","created":1489079981393,"exp":1489684781}
* signature的生成算法:
* HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
*/
@Slf4j
@Component
public class JwtTokenUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
// private static final String CLAIM_KEY_USERNAME = "sub";
// private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.secret}")
private String secret;// 盐
@Value("${jwt.expiration}")
private Long expiration;
/**
* 根据 负载(用户名 部门 权限 等) 生成JWT的token
*/
/* private String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}*/
/**
* 从token中获取JWT中的负载
*/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
LOGGER.info("JWT格式验证失败:{}",token);
}
return claims;
}
/**
* 生成token的过期时间 30秒过期
*/
// private Date generateExpirationDate() {
// return new Date(System.currentTimeMillis() + expiration * 1000);
// }
/**
* 从token中获取登录用户名
*/
public String getUserNameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 验证token是否还有效
* @param token 客户端传入的token
* @param userDetails 从数据库中查询出来的用户信息
*/
public boolean validateToken(String token, UserDetails userDetails) {
String username = getUserNameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 判断token是否已经失效
*/
private boolean isTokenExpired(String token) {
Date expiredDate = getExpiredDateFromToken(token);
return expiredDate.before(new Date());
}
/**
* 从token中获取过期时间
*/
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
/**
* 根据用户信息生成token
*/
public String generateToken(UserDetails userDetails) {
/* Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);*/
String token = generateJwtToken(userDetails);
return token;
}
String generateJwtToken(UserDetails userDetails) {
// claim:Playload 里的申明部分
Map<String, Object> claims = new HashMap<>();
// 创建一个jwtbuilder负责构造一个jwt的对象,使用的是设计模式里的建造者模式(Builder)
JwtBuilder jwtBuilder = Jwts.builder();
jwtBuilder.setClaims(claims);
// 设置claims的主题
jwtBuilder.setSubject(userDetails.getUsername());
// 设置claims里的iat:签发的时间
long iat = System.currentTimeMillis();
log.info("iat:{}",iat);
jwtBuilder.setIssuedAt(new Date(iat));
// 设置claims里的exp:过期时间
long exp = System.currentTimeMillis() + expiration;
log.info("exp:{}",exp);
jwtBuilder.setExpiration(new Date(exp));
// 使用指定的算法进行签名,生成一个jws
jwtBuilder.signWith(SignatureAlgorithm.HS512, secret);
//构建JWT并将其序列化为一个紧凑的、url安全的字符串
String token = jwtBuilder.compact();
return token;
}
//
// /**
// * 判断token是否可以被刷新
// */
// public boolean canRefresh(String token) {
// return !isTokenExpired(token);
// }
//
// /**
// * 刷新token
// */
// public String refreshToken(String token) {
// Claims claims = getClaimsFromToken(token);
// claims.put(CLAIM_KEY_CREATED, new Date());
// return generateToken(claims);
// }
}
2.5 创建Result结果集
依次创建一下四个实体类。
1)CommonResult 标准返回类
package com.wanxi.springboot1018.result;
/**
* 通用返回对象
*
*/
public class CommonResult<T> {
private long code;
private String message;
private T data;
protected CommonResult() {
}
protected CommonResult(long code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
/**
* 成功返回结果
*
* @param data 获取的数据
*/
public static <T> CommonResult<T> success(T data) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
/**
* 成功返回结果
*
* @param data 获取的数据
* @param message 提示信息
*/
public static <T> CommonResult<T> success(T data, String message) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data);
}
/**
* 失败返回结果
* @param resultCode 错误码
*/
public static <T> CommonResult<T> failed(ResultCode resultCode) {
return new CommonResult<T>(resultCode.getCode(), resultCode.getMessage(), null);
}
/**
* 失败返回结果
* @param message 提示信息
*/
public static <T> CommonResult<T> failed(String message) {
return new CommonResult<T>(ResultCode.FAILED.getCode(), message, null);
}
/**
* 失败返回结果
*/
public static <T> CommonResult<T> failed() {
return failed(ResultCode.FAILED);
}
/**
* 参数验证失败返回结果
*/
public static <T> CommonResult<T> validateFailed() {
return failed(ResultCode.VALIDATE_FAILED);
}
/**
* 参数验证失败返回结果
* @param message 提示信息
*/
public static <T> CommonResult<T> validateFailed(String message) {
return new CommonResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message, null);
}
/**
* 未登录返回结果
*/
public static <T> CommonResult<T> unauthorized(T data) {
return new CommonResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data);
}
/**
* 未授权返回结果
*/
public static <T> CommonResult<T> forbidden(T data) {
return new CommonResult<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data);
}
public long getCode() {
return code;
}
public void setCode(long code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
2)RestAuthenticationEntryPoint 认证入口
package com.wanxi.springboot1018.result;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 当未登录或者token失效访问接口时,自定义的返回结果
*/
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Resource
ObjectMapper objectMapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
String jsonString = objectMapper.writeValueAsString(CommonResult.unauthorized(authException.getMessage()));
response.getWriter().println(jsonString);
response.getWriter().flush();
}
}
3)RestfulAccessDeniedHandler 拒绝访问处理器
package com.wanxi.springboot1018.result;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 当访问接口没有权限时,自定义的返回结果
*/
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
@Resource
ObjectMapper objectMapper;
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException e) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
String jsonString = objectMapper.writeValueAsString(CommonResult.forbidden(e.getMessage()));
response.getWriter().println(jsonString);
response.getWriter().flush();
}
}
4)ResultCode 状态码
package com.wanxi.springboot1018.result;
/**
* 枚举了一些常用API操作码
*/
public enum ResultCode {
/**
* 返回200的状态码,表示成功。这个嘛尽量和http的码含义一致。
*/
SUCCESS(200, "success"),
/**
*
*/
FAILED(500, "操作失败"),
/**
*
*/
VALIDATE_FAILED(404, "参数检验失败"),
/**
*
*/
UNAUTHORIZED(401, "暂未登录或token已经过期"),
/**
*
*/
FORBIDDEN(403, "没有相关权限");
private long code;
private String message;
ResultCode(long code, String message) {
this.code = code;
this.message = message;
}
public long getCode() {
return code;
}
public String getMessage() {
return message;
}
}
2.6 创建LoginController
package com.wanxi.springboot1018.controller;
import com.wanxi.springboot1018.entity.LoginParams;
import com.wanxi.springboot1018.entity.Result;
import com.wanxi.springboot1018.entity.User;
import com.wanxi.springboot1018.result.CommonResult;
import com.wanxi.springboot1018.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RestController
@CrossOrigin
public class LoginController {
@Resource
RedisTemplate redisTemplate;
@Value("${jwt.tokenHead}")
String tokenHead;
@Autowired
UserService userService;
//传入参数时json格式的 LoginParams 对象
@PostMapping("/login")
public CommonResult login(@RequestBody LoginParams loginParams){
HashMap<String, String> data = new HashMap<>();
String token = null;
//根据用户信息获取token
try {
token = userService.login(loginParams);
//存入redis
//redisTemplate.boundValueOps(token).set(loginParams.getUsername(), 5, TimeUnit.MINUTES);
} catch (Exception e) {
e.printStackTrace();
return CommonResult.validateFailed("用户名或密码错误");
}
if (StringUtils.isEmpty(token)){
return CommonResult.validateFailed("非法token");
}//可能会抛出异常
data.put("tokenHead",tokenHead);
data.put("access_token",token);
// localStorage.setItem("Authorization","Bearer sdsdfdfds")
// $ajax{data:{},type:"",header:{"Authorization":"Bearer sdsdfdfds"}}
return CommonResult.success(data);
}
}
另外展示一下除了登录接口的另外两个用于测试的接口:/index 和/users 分别是无限制和权限限制的。
package com.wanxi.springboot1018.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class IndexController {
@RequestMapping("/index")
public String getAllUsers() {
log.info("访问 index..");
return "访问 index....";
}
@RequestMapping("/users")
@PreAuthorize("hasAuthority('cx:updates_user')")// 授权:有cx:updates_user权限才能做该操作 否则报错403
public String update() {
SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication authentication = securityContext.getAuthentication();
return authentication.toString();
}
}
2.7 新增一个entity(保留了之前的entity)
不同与之前的采用的POJO数据参数直接获取user,重新写了一个参数类。方便识别(必须有 username、password两个参数,前端也需要按照这个格式发送json数据)
package com.wanxi.springboot1018.entity;
import lombok.Data;
import java.io.Serializable;
//LoginParams 登录参数类:包含用户名和密码.
@Data
public class LoginParams implements Serializable {
private String username;
private String password;
}
2.8 修改yml文件
相比之前的代码,增加了jwt配置
jwt:
#盐
secret: mySecret
#过期时间
expiration: 1800000
#
tokenHead: bearer
#
tokenHeader: Authorization
server:
port: 9090
servlet:
encoding:
enabled: false #使自定义过滤器生效
spring:
datasource: #jdbc驱动连接数据库
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: jinghongjie
url: jdbc:mysql://localhost:3306/jing?characterEncoding=utf8
mvc: # swagger 使用 不写这个启动会出错
pathmatch:
matching-strategy: ant_path_matcher
jpa: # 配置jpa
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
format_sql: true
show-sql: true
hibernate:
ddl-auto: update
redis: #配置redis
connect-timeout: 100ms # 连接超时时间 默认100ms
lettuce: # spring 默认使用的是lettuce连接池,配置连接池相关属性
pool: #采用连接池的方式
max-active: 8 #最大连接数
max-wait: 100ms #连接池耗尽时阻塞的最大时间
max-idle: 200 #最大空闲连接数
min-idle: 5 # 连接池里维护的最小空闲连接数 默认0
mybatis:
mapperLocations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
jwt:
#盐
secret: mySecret
#过期时间
expiration: 1800000
#
tokenHead: bearer
#
tokenHeader: Authorization
代码完善到这里,基本就可以实现用户的token认证了。
2.9 认证测试
先访问登录接口进行访问,验证token的正常生成
成功返回了下面的token (生效30分钟)
现在我们模拟客户端,带上Authrization请求头(包含token)访问 /index接口。
注意:必须按照bear+空格+token的格式放入Authrization请求头
可以看到能够正常访问接口
换个接口测试:同样的,访问/users接口(有权限设置)
当前测试概念图:
3 浅谈JWT在开发应用中的优缺点
通过刚才的代码演示,我们看到了JWT的原理和特点:
3.1 JWT的优势
- 当用户在多地同时登录时,一个用户的token在客户端是唯一的,因此会按照最新的一次登录返回的token为准,也就是说只能一个地方登录。
-
JWT通过非对称加密技术实现,有效避免了 CSRF 攻击CSRF(Cross Site Request Forgery)跨站请求伪造(简单来说就是用你的身份去做一些不好的事情,发送一些对你不友好的请求比如恶意转账)
-
无状态的JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息(尤其是在用户量及其庞大的时候)。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。
当然还有一些 JWT 还可以跨语言使用、 适合移动端应用等优点就不细说了。
3.2 JWT的不足
- 用户只能在一个设备上登录,但是一个用户可能需要多地登录,实现多设备登录。
- 由于jwt的payload是使用base64编码的,并没有加密,因此jwt中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全。
- jwt使用过时间对用户的token进行期限限制的,当我们需要更改这个期限的时候,就不是很方便了,至少不能当做变量直接修改。