1.1 什么是JWT?
1.2 JWT是干嘛的 以及 JWT和SessionToken的区别
1.3 JWT的工作原理(以最简单的模型为例)
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.1 JWT的优势
3.2 JWT的不足
1.1 什么是JWT?
JWT:JSON Web Tokens
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
本文代码是基于Security的基础上进行的改进和增强
这是我的上一篇:
SpringBoot中的Security简单入门实现_敬叫唤的博客-CSDN博客
2.1 导入maven依赖
只增加了
1.8
8.0.30
1.3.2
org.springframework.boot
spring-boot-starter
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-web
org.mybatis.spring.boot
mybatis-spring-boot-starter
${mybatis.springboot.version}
mysql
mysql-connector-java
${mysql.version}
com.github.pagehelper
pagehelper-spring-boot-starter
1.4.3
com.github.pagehelper
pagehelper
5.3.1
org.springframework.boot
spring-boot-starter-data-jpa
mysql
mysql-connector-java
io.springfox
springfox-swagger2
2.9.2
io.springfox
springfox-swagger-ui
2.9.2
net.minidev
json-smart
org.springframework.boot
spring-boot-starter-data-redis
org.aspectj
aspectjweaver
1.9.7
compile
org.springframework.boot
spring-boot-starter-security
io.jsonwebtoken
jjwt
0.9.1
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 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 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 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 {
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 CommonResult success(T data) {
return new CommonResult(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
/**
* 成功返回结果
*
* @param data 获取的数据
* @param message 提示信息
*/
public static CommonResult success(T data, String message) {
return new CommonResult(ResultCode.SUCCESS.getCode(), message, data);
}
/**
* 失败返回结果
* @param resultCode 错误码
*/
public static CommonResult failed(ResultCode resultCode) {
return new CommonResult(resultCode.getCode(), resultCode.getMessage(), null);
}
/**
* 失败返回结果
* @param message 提示信息
*/
public static CommonResult failed(String message) {
return new CommonResult(ResultCode.FAILED.getCode(), message, null);
}
/**
* 失败返回结果
*/
public static CommonResult failed() {
return failed(ResultCode.FAILED);
}
/**
* 参数验证失败返回结果
*/
public static CommonResult validateFailed() {
return failed(ResultCode.VALIDATE_FAILED);
}
/**
* 参数验证失败返回结果
* @param message 提示信息
*/
public static CommonResult validateFailed(String message) {
return new CommonResult(ResultCode.VALIDATE_FAILED.getCode(), message, null);
}
/**
* 未登录返回结果
*/
public static CommonResult unauthorized(T data) {
return new CommonResult(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data);
}
/**
* 未授权返回结果
*/
public static CommonResult forbidden(T data) {
return new CommonResult(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 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接口(有权限设置)
当前测试概念图:
通过刚才的代码演示,我们看到了JWT的原理和特点:
3.1 JWT的优势
JWT通过非对称加密技术实现,有效避免了 CSRF 攻击CSRF(Cross Site Request Forgery)跨站请求伪造(简单来说就是用你的身份去做一些不好的事情,发送一些对你不友好的请求比如恶意转账)
无状态的JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息(尤其是在用户量及其庞大的时候)。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。
当然还有一些 JWT 还可以跨语言使用、 适合移动端应用等优点就不细说了。
3.2 JWT的不足