day76(Spring Security + JWT,JwtAuthenticationFilter)
Spring Security + JWT
1.将返回的数据改为JWT数据
2.JWT组成
1.Header(头):指定算法和当前数据类型
2.Payload(载荷):Claims(自定义数据)和过期时间
3.Signature(签名):算法和密钥
3.代码
@Service
public class AdminServiceImpl implements IAdminService {
// ===== 原有其它代码 =====
/**
* JWT数据的密钥
*/
private String secretKey = "fgfdsfadsfadsafdsafdsfadsfadsfdsafdasfdsafdsafdsafds4rttrefds";
@Override
public String login(AdminLoginDTO adminLoginDTO) {
// ===== 原有其它代码 =====
// 如果程序可以执行到此处,则表示登录成功
// 生成此用户数据的JWT
// Claims
User user=(User) authenticate.getPrincipal();
System.out.println("从认证结果中获取Principal=" + user.getClass().getName());
Map<String,Object> claims=new HshMap<>();
claims.put("username",user.getUsername());
claims.put("permissions",user.getAuthorities());
System.out.println("即将向JWT中写入数据=" + claims);
//JWT的组成部分:Header(头),Payload(载荷),Signature(签名)
String jwt=Jwts.builder()
//Header:指定算法与当前数据类型
//格式:{"alg":算法,"typ":"jwt"}
.setHeaderParam(Header.CONTENT_TYPE,"HS256")
.setHeaderParam(Header.TYPE,Header.JWT_TYPE)
//Payload:通常包含Claims(自定义数据)和过期时间
.setClaims(claims)
.setExpiration(new Data(System.currentTimeMillis()+5*60*1000))
//Signature:由算法和密钥(secret key)两部分组成
.signwith(SignatureAlgorithm.HS256,secretkey)
//打包生成
.compact();
// 返回JWT数据
return jwt;
}
}
4.更改控制器处理请求的返回值的类型(AdminController)
1.原因:
因为在控制器中,应该响应的是JSON格式的数据,所以,需要在csmall-passport
中添加依赖csmall-common
并将控制器处理请求的方法的返回值类型改为JsonResult<String>
,并调整返回值:
2.代码:
//http://localhost:8080/admins/login?username=root&password=123456
@RequestMapping("/login")
public JsonResult<String> login(AdminLoginDTO adminLoginDTO){
String jwt=adminService.login(adminLoginDTO);
return JsonResult.ok(jwt);
}
3.结果:
{
"state":20000,
"message":null,
"data":"eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJwZXJtaXNzaW9ucyI6W3siYXV0aG9yaXR5IjoiL2Ftcy9hZG1pbi9kZWxldGUifSx7ImF1dGhvcml0eSI6Ii9hbXMvYWRtaW4vcmVhZCJ9LHsiYXV0aG9yaXR5IjoiL2Ftcy9hZG1pbi91cGRhdGUifSx7ImF1dGhvcml0eSI6Ii9wbXMvcHJvZHVjdC9kZWxldGUifSx7ImF1dGhvcml0eSI6Ii9wbXMvcHJvZHVjdC9yZWFkIn0seyJhdXRob3JpdHkiOiIvcG1zL3Byb2R1Y3QvdXBkYXRlIn1dLCJleHAiOjE2NTU0MzQwMzcsInVzZXJuYW1lIjoicm9vdCJ9.8ZIfpxxjJlwNo-E3JhXwH4sZR0J5-FU-HAOMu1Tg-44"
}
注意:以上只是访问/admin/login
时会执行所编写的流程(发送用户名和密码,得到含JWT的结果),并不代表真正意义的实现了“登录”!
5.登录流程
6.添加Knife4j(便于体现客户端携带JWT)
且在使用Knife4j时,需要在白名单中添加相关资源路径
SecurityConfiguration
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// ===== 原有其它代码 =====
@Override
protected void configure(HttpSecurity http) throws Exception {
// ===== 原有其它代码 =====
// URL白名单
String[] urls = {
"/admins/login",
"/doc.html", // 从本行开始,以下是新增
"/**/*.js",
"/**/*.css",
"/swagger-resources",
"/v2/api-docs",
"/favicon.ico"
};
// ===== 原有其它代码 =====
}
}
7.在控制器添加测试访问的请求配置:便于测试(AdminController)
// 以下是测试访问的请求
@GetMapping("/hello")
public String sayHello() {
return "hello~~~";
}
由于/admins/hello不在白名单中,直接访问会出现403错误
8.使用规范
- JWT数据必须携带在请求头(Request Header)的Authorization属性中
- 服务器端在每次接收到请求后
- 先判断请求头是否存在Authorization
- Authorization的值是否有效等操作
通过过滤器实现以上检查
9.过滤器
1.创建过滤类(JwtAuthenticationFilter)
在csmall-passport
的根包下的security
包下创建JwtAuthenticationFilter
过滤器类,需要继承自OncePerRequestFilter
类:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws servletexception, IOException {
System.out.println("JwtAuthenticationFilter.doFilterInternal()");
}
}
2.将自定义过滤器注册在Spring Security的相关过滤器之前
原因:
-
以上用于验证JWT的过滤器应该运行在Spring Security处理登录的过滤器之前,需要在自定义的
SecurityConfiguration
中的configure()
方法中将以上自定义的过滤器注册在Spring Security的相关过滤器之前 -
@Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { // 新增 @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; // ===== 原有其它代码 ===== @Override protected void configure(HttpSecurity http) throws Exception { // ===== 原有其它代码 ===== // 注册处理JWT的过滤器 // 此过滤器必须在Spring Security处理登录的过滤器之前 http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } }
10.完成后表现
完成后,重启项目,无论对哪个路径发出请求,在控制台都可以看出输出了过滤器中的输出语句内容,并且,在浏览器将显示一片空白。
11.JwtAuthenticationFilter
需要实现:
- 尝试从请求头中获取JWT数据
package cn.tedu.csmall.passport.security;
import cn.tedu.csmall.common.web.JsonResult;
import cn.tedu.csmall.common.web.State;
import com.alibaba.fastjson.JSON;
import io.jsonwebtoken.*;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
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;
import java.util.List;
/**
* JWT过滤器:从请求头的Authorization中获取JWT中存入的用户信息
* 并添加到Spring Security的上下文中
* 以致于Spring Security后续的组件(包括过滤器等)能从上下文中获取此用户的信息
* 从而验证是否已经登录、是否具有权限等
*/
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
/**
* JWT数据的密钥
*/
private String secretKey = "fgfdsfadsfadsafdsafdsfadsfadsfdsafdasfdsafdsafdsafds4rttrefds";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws servletexception, IOException {
System.out.println("JwtAuthenticationFilter.doFilterInternal()");
// 清除Spring Security上下文中的数据
// 避免此前曾经存入过用户信息,后续即使没有携带JWT,在Spring Security仍保存有上下文数据(包括用户信息)
System.out.println("清除Spring Security上下文中的数据");
SecurityContextHolder.clearContext();
// 客户端提交请求时,必须在请求头的Authorization中添加JWT数据,这是当前服务器程序的规定,客户端必须遵守
// 尝试获取JWT数据
String jwt = request.getHeader("Authorization");
System.out.println("从请求头中获取到的JWT=" + jwt);
// 判断是否不存在jwt数据
if (!StringUtils.hasText(jwt)) {
// 不存在jwt数据,则放行,后续还有其它过滤器及相关组件进行其它的处理,例如未登录则要求登录等
// 此处不宜直接阻止运行,因为“登录”、“注册”等请求本应该没有jwt数据
System.out.println("请求头中无JWT数据,当前过滤器将放行");
filterChain.doFilter(request, response); // 继续执行过滤器链中后续的过滤器
return; // 必须
}
// 注意:此时执行时,如果请求头中携带了Authentication,日志中将输出,且不会有任何响应,因为当前过滤器尚未放行
// 以下代码有可能抛出异常的
// Todo 密钥和各个Key应该统一定义
String username = null;
String permissionsstring = null;
try {
System.out.println("请求头中包含JWT,准备解析此数据……");
Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
username = claims.get("username").toString();
permissionsstring = claims.get("permissions").toString();
System.out.println("username=" + username);
System.out.println("permissionsstring=" + permissionsstring);
} catch (ExpiredJwtException e) {
System.out.println("解析JWT失败,此JWT已过期:" + e.getMessage());
JsonResult<Void> jsonResult = JsonResult.fail(
State.ERR_JWT_EXPIRED, "您的登录已过期,请重新登录!");
String jsonString = JSON.toJSONString(jsonResult);
System.out.println("响应结果:" + jsonString);
response.setContentType("application/json; charset=utf-8");
response.getWriter().println(jsonString);
return;
} catch (MalformedJwtException e) {
System.out.println("解析JWT失败,此JWT数据错误,无法解析:" + e.getMessage());
JsonResult<Void> jsonResult = JsonResult.fail(
State.ERR_JWT_MALFORMED, "获取登录信息失败,请重新登录!");
String jsonString = JSON.toJSONString(jsonResult);
System.out.println("响应结果:" + jsonString);
response.setContentType("application/json; charset=utf-8");
response.getWriter().println(jsonString);
return;
} catch (SignatureException e) {
System.out.println("解析JWT失败,此JWT签名错误:" + e.getMessage());
JsonResult<Void> jsonResult = JsonResult.fail(
State.ERR_JWT_SIGNATURE, "获取登录信息失败,请重新登录!");
String jsonString = JSON.toJSONString(jsonResult);
System.out.println("响应结果:" + jsonString);
response.setContentType("application/json; charset=utf-8");
response.getWriter().println(jsonString);
return;
} catch (Throwable e) {
System.out.println("解析JWT失败,异常类型:" + e.getClass().getName());
e.printstacktrace();
JsonResult<Void> jsonResult = JsonResult.fail(
State.ERR_INTERNAL_SERVER_ERROR, "获取登录信息失败,请重新登录!");
String jsonString = JSON.toJSONString(jsonResult);
System.out.println("响应结果:" + jsonString);
response.setContentType("application/json; charset=utf-8");
response.getWriter().println(jsonString);
return;
}
// 将此前从JWT中读取到的permissionsstring(JSON字符串)转换成Collection<? extends GrantedAuthority>
List<SimpleGrantedAuthority> permissions
= JSON.parseArray(permissionsstring, SimpleGrantedAuthority.class);
System.out.println("从JWT中获取到的权限转换成Spring Security要求的类型:" + permissions);
// 将解析得到的用户信息传递给Spring Security
// 获取Spring Security的上下文,并将Authentication放到上下文中
// 在Authentication中封装:用户名、null(密码)、权限列表
// 因为接下来并不会处理认证,所以Authentication中不需要密码
// 后续,Spring Security发现上下文中有Authentication时,就会视为已登录,甚至可以获取相关信息
Authentication authentication
= new UsernamePasswordAuthenticationToken(username, null, permissions);
SecurityContextHolder.getContext().setAuthentication(authentication);
System.out.println("将解析得到的用户信息传递给Spring Security");
// 放行
System.out.println("JwtAuthenticationFilter 放行");
filterChain.doFilter(request, response);
}
}
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。