- 前言
- 一、准备工作
- 二、关于JWT
- session
- Token
- 三、关于JWT在项目中的应用
- 生成JWT
- 关于业务实现类的登录方法:
- 四、 在服务器端检查并解析JWT
- 总结
前言
提示:这里可以添加本文要记录的大概内容:
之前介绍了Spring Security是主要解决认证(Authenticate)和授权(Authorization)的框架。详细见关于Spring Security
下面主要介绍如何使用Security开发自定义的登录流程
提示:以下是本篇文章正文内容,下面案例可供参考
一、准备工作此前,登录是由Security框架提供的页面的表单来输入用户名、密码,且由Security框架自动处理登录流程,不适合前后端分离的开发模式!所以,需要自行开发登录流程!
关于自定义的登录流程,主要需要:
- 在业务逻辑实现类中,调用Security的验证机制来执行登录认证
- 在控制器类中,自定义处理请求,用于接收登录请求及请求参数,并调用业务逻辑实现类实现认证
关于在Service中调用Security的认证机制:
当需要调用Security框架的认证机制时,需要使用AuthenticationManager对象,可以在Security配置类中重写authenticationManager()方法,在此方法上添加@Bean注解,由于当前类本身是配置类,所以Spring框架会自动调用此方法,并将返回的结果保存到Spring容器中:
@Bean @Override protected AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager(); }
在IAdminService中添加处理登录的抽象方法:
String login(AdminLoginDTO adminLoginDTO);
在AdminServiceImpl中,可以自动装配AuthenticationManager对象,并实现接口中的方法:
@Autowired private AuthenticationManager authenticationManager; @Override public void login(AdminLoginDTO adminLoginDTO) { // 日志 log.debug("开始处理【管理员登录】的业务,参数:{}", adminLoginDTO); // 调用AuthenticationManager执行认证,创建认证对象,即传入用户名和密码 Authentication authentication = new UsernamePasswordAuthenticationToken( adminLoginDTO.getUsername(), adminLoginDTO.getPassword()); //走Spring Security的认证流程,即loadUserByUsername(String s) authenticationManager.authenticate(authentication); log.debug("认证通过!");
在根包下创建pojo.dto.AdminLoginDTO类:
@Data public class AdminLoginDTO implements Serializable { private String username; private String password; }
在AdminController中添加处理请求的方法:
@ApiOperation("管理员登录") @ApiOperationSupport(order = 50) @PostMapping("/login") public JsonResultlogin(AdminLoginDTO adminLoginDTO) { log.debug("准备处理【管理员登录】的请求:{}", adminLoginDTO); adminService.login(adminLoginDTO); return JsonResult.ok(); }
为了保证能对以上路径直接发起请求,需要将此路径(/admins/login)添加到Security配置类的“白名单”中。
完成后,启动项目,可以通过Knife4j的调试来测试登录,当登录成功时将响应正确,当用户名或密码错误时,将响应错误(需要统一处理异常)。
**注意:即使登录成功,也不可以实现其它请求的访问
二、关于JWT sessionHTTP协议本身是无状态协议,所以,无法识别用户的身份!
为了解决此问题,经编程时,引入了Session机制,用于保存用户的某些信息,可识别用户的身份!
Session的本身是在服务器端的内存中一个类似Map结构的数据,每个客户端在提交请求时,都会携带一个由服务器端首次响应时分配的Session ID,作为Map的Key,由于此Session ID具有极强的唯一性,所以,每个客户端的Session ID理论上都是不相同的,从而服务器可以识别客户端!
由于Session是保存在服务器端的内存中的,在一般使用时,并不适用于集群!
TokenToken:令牌,票据。
目前,推荐使用Token来保存用户的身份标识,使之可以用于集群!
相比Session ID是没有信息含义的,Token则是有信息含义的数据,当客户端向服务器端提交登录请求后,服务器商认证通过就会将此用户的信息保存在Token中,并将此Token响应到客户端,后续,客户端在每次请求时携带Token,服务器端即可识别用户的身份!
JWT = JSON Web Token
JWT是使用JSON格式表示一系列的数据的Token。
当需要使用JWT时,应该在项目中添加依赖:
io.jsonwebtoken jjwt 0.9.1
然后,通过测试,实现生成JWT和解析JWT
@Slf4j public class JwtTests { // 密钥(盐),用于生成jwt和解析jwt String secretKey = "nmlfdasfdsaurefuifdknjfdskjhajhef"; // 测试生成JWT @Test public void testGenerateJwt() { // 准备Claims Map三、关于JWT在项目中的应用 生成JWTclaims = new HashMap<>(); claims.put("id", 9527); claims.put("name", "liulaoshi"); // JWT的组成部分:Header(头),Payload(载荷),Signature(签名) String jwt = Jwts.builder() // Header:用于声明算法与此数据的类型,以下配置的属性名是固定的 .setHeaderParam("alg", "HS256") .setHeaderParam("typ", "jwt") // Payload:用于添加自定义数据,并声明有效期 .setClaims(claims) .setExpiration(new Date(System.currentTimeMillis() + 3 * 60 * 1000)) // Signature:用于指定算法与密钥(盐) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); log.debug("JWT = {}", jwt); // eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.eyJuYW1lIjoibGl1bGFvc2hpIiwiaWQiOjk1MjcsImV4cCI6MTY1OTkzOTUzMH0.lwD_PzrqGXEgQs3KmMjsYzTmhsKbGhKnd1WkDkFpj5M } @Test public void testParseJwt() { //解析JWT String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.eyJuYW1lIjoibGl1bGFvc2hpIiwiaWQiOjk1MjcsImV4cCI6MTY1OTkzOTUzMH0.lwD_PzrqGXEgQs3KmMjsYzTmhsKbGhKnd1WkDkFpj5M"; Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody(); Object id = claims.get("id"); Object name = claims.get("name"); log.debug("id={}", id); log.debug("name={}", name); } }
应该在用户登录时,视为”认证成功“后,生成JWT,并将此数据响应到客户端。
在业务层,调用AuthenticationManager的authenticate()方法后,得到的返回结果例如:
UsernamePasswordAuthenticationToken [ Principal=org.springframework.security.core.userdetails.User [ Username=root, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[权限列表...] ], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[权限列表...] ]
可以看到,认证返回的数据中将包含成功认证的用户信息,也是当初用于执行认证的信息(UserDetailsServiceImpl中返回的结果),可以从此认证结果中获取用户相关数据,并写入到JWT中,则需要:
- 将业务接口中的登录方法返回值类型改为String,表示认证成功后返回的JWT
- 将业务实现类中的登录方法返回值一并修改
- 在业务实现类中,当认证成功后,获取需要写入到JWT中的数据(例如:用户名等),并生成JWT,返回JWT
@Override public String login(AdminLoginDTO adminLoginDTO) { // 日志 log.debug("开始处理【管理员登录】的业务,参数:{}", adminLoginDTO); // 调用AuthenticationManager执行认证 Authentication authentication = new UsernamePasswordAuthenticationToken( adminLoginDTO.getUsername(), adminLoginDTO.getPassword()); Authentication authenticateResult = authenticationManager.authenticate(authentication); log.debug("认证通过,返回的结果:{}", authenticateResult); log.debug("认证结果中的Principal的类型:{}", authenticateResult.getPrincipal().getClass().getName()); //org.springframework.security.core.userdetails.User类型 // 处理认证结果 User loginUser = (User) authenticateResult.getPrincipal(); log.debug("认证结果中的用户名:{}", loginUser.getUsername()); // 生成JWT String secretKey = "nmlfdasfdsaurefuifdknjfdskjhajhef"; // 准备Claims Mapclaims = new HashMap<>(); claims.put("username", loginUser.getUsername()); // JWT的组成部分:Header(头),Payload(载荷),Signature(签名) String jwt = Jwts.builder() // Header:用于声明算法与此数据的类型,以下配置的属性名是固定的 .setHeaderParam("alg", "HS256") .setHeaderParam("typ", "jwt") // Payload:用于添加自定义数据,并声明有效期 .setClaims(claims) .setExpiration(new Date(System.currentTimeMillis() + 14 * 24 * 60 * 60 * 1000)) // Signature:用于指定算法与密钥(盐) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); log.debug("生成的JWT:{}", jwt); return jwt; }
在控制器中,将处理登录请求的方法的返回值类型改为JsonResult
@ApiOperation("管理员登录") @ApiOperationSupport(order = 50) @PostMapping("/login") public JsonResultlogin(AdminLoginDTO adminLoginDTO) { log.debug("准备处理【管理员登录】的请求:{}", adminLoginDTO); String jwt = adminService.login(adminLoginDTO); return JsonResult.ok(jwt); }
完成后,重启项目,在Knife4j的调试功能中,使用正常的用户名和密码发起登录请求,将响应JWT结果,例如:
{ "state": 20000, "data": "eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.eyJleHAiOjE2NjExNTIzOTUsInVzZXJuYW1lIjoic3VwZXJfYWRtaW4ifQ.rFACBsBY8w8oNpR80n2YiplsEUIqw5bnCIsC5UAqsww" }四、 在服务器端检查并解析JWT
经过以上登录认证并响应JWT后,客户端在后续发起请求时,应该自主携带JWT数据,而服务器端应该尝试检查并解析JWT。
由于客户端在发起多种不同请求时都应该携带JWT,且服务器端都应该检查并尝试解析,所以,服务器端检查并解析的过程,应该发生在比较”通用“的组件中,即无论客户端提交的是哪个路径的请求,这个组件都应该执行!通常,会使用过滤器组件进行处理。
在项目的根包下创建filter.JwtAuthrozationFilter类,继承自OncePerRequestFilter,并在此类上添加@Component注解:
@Slf4j @Component public class JwtAuthorizationFilter extends OncePerRequestFilter { public JwtAuthorizationFilter() { log.debug("创建过滤器:JwtAuthorizationFilter"); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { log.debug("执行JwtAuthorizationFilter"); // 过滤器链继续执行,相当于:放行 filterChain.doFilter(request, response); } }
关于客户端提交请求时携带JWT数据,业内通用的做法是在请求头中添加Authorization属性,其值就是JWT数据,所以,服务器端获取JWT的做法是:从请求头中的Authorization属性中获取JWT数据!
@Slf4j @Component public class JwtAuthorizationFilter extends OncePerRequestFilter { public JwtAuthorizationFilter() { log.debug("创建过滤器:JwtAuthorizationFilter"); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { log.debug("执行JwtAuthorizationFilter"); // 从请求头中获取JWT String jwt = request.getHeader("Authorization"); log.debug("从请求头中获取JWT:{}", jwt); // 判断JWT数据是否不存在 if (!StringUtils.hasText(jwt) || jwt.length() < 80) { log.debug("获取到的JWT是无效的,直接放行,交由后续的组件继续处理!"); // 过滤器链继续执行,相当于:放行 filterChain.doFilter(request, response); // 返回,终止当前方法本次执行 return; } // 尝试解析JWT String secretKey = "nmlfdasfdsaurefuifdknjfdskjhajhef"; Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody(); Object username = claims.get("username"); log.debug("从JWT中解析得到username:{}", username); // 准备Authentication对象,后续会将此对象封装到Security的上下文中 Listauthorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("临时使用的权限")); Authentication authentication = new UsernamePasswordAuthenticationToken( username, null, authorities); // 将用户信息封装到Security的上下文中 SecurityContext securityContext = SecurityContextHolder.getContext(); securityContext.setAuthentication(authentication); log.debug("已经向Security的上下文中写入:{}", authentication); // 过滤器链继续执行,相当于:放行 filterChain.doFilter(request, response); } }
完成后,还需要将此过滤器添加在Security框架的UsernamePasswordAuthenticationFilter过滤器之前,需要在Security配置类中,先自动装配自定义的过滤器对象:
@Autowired private JwtAuthorizationFilter jwtAuthorizationFilter;
然后,在configurer(HttpSecurity http)方法中添加:
// 将“JWT过滤器”添加在“认证过滤器”之前 http.addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class);
最后,在JWT过滤器执行之初,先清除Security上下文中的数据,以避免”一旦提交JWT将认证对象存入到Security上下文中,后续不携带JWT也能访问“的问题:
// 清除Security上下文中的数据 SecurityContextHolder.clearContext();
完成后,启动项目,在Knife4j的调试功能中,携带JWT可以发起任何需要登录才能访问的请求,反之,这些请求不携带JWT将不允许访问。
总结提示:这里对文章进行总结:
以上就是今天要讲的内容,本文介绍了如何使用Security开发自定义的登录流程,以及如何将JWT在项目中的应用。简而言之就是在用户登录时,视为”认证成功“后,生成JWT,并将此数据响应到客户端。经过以上登录认证并响应JWT后,客户端在后续发起请求时,应该自主携带JWT数据,而服务器端应该尝试检查并解析JWT。