AI写作智能体 自主规划任务,支持联网查询和网页读取,多模态高效创作各类分析报告、商业计划、营销方案、教学内容等。 广告
我们本节要实现的需求是:用户发起登录认证请求,网关服务上对该用户进行认证(用户名密码),认证成功之后将JWT令牌返回给用户客户端。 ![](https://img.kancloud.cn/38/de/38de5cf13d3b5ddf371781d63aca6328_411x195.png) 实现完成之后的项目结构如下: ![](https://img.kancloud.cn/df/5b/df5bbb4fc78b7c54b14a1be32ad5966c_375x430.png) ## 一、maven核心依赖 在上一章代码的基础上,加上如下的一些maven依赖 ~~~ <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-crypto</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> ~~~ * jjwt是实现JWT 令牌的核心类库 * spring-boot-starter-data-jpa是持久层框架,因为我们需要去数据库加载用户信息。之所以不用mybatis,因为webFlux下mybatis目前兼容性不好。 * spring-security-crypto是Spring 框架下进行加密解密、加签解签操作的常用类库 ## 二、核心Controller 2个核心函数: * authentication实现登录认证,认证成功之后返回JWT令牌 * refreshtoken实现令牌刷新,使用旧的令牌换取新的令牌(因为JWT令牌是有有效期的,超过有效期令牌非法) > 注意下文中的Mono是WebFlux结果响应数据回调的做法,不是我的自定义。 ~~~ /** * JWT获取令牌和刷新令牌接口 */ @RestController @ConditionalOnProperty(name = "zimug.gateway.jwt.useDefaultController", havingValue = "true") public class JwtAuthController { @Resource private JwtProperties jwtProperties; @Resource private SysUserRepository sysUserRepository; @Resource private JwtTokenUtil jwtTokenUtil; @Resource private PasswordEncoder passwordEncoder; /** * 使用用户名密码换JWT令牌 */ @RequestMapping("/authentication") public Mono<AjaxResponse> authentication(@RequestBody Map<String,String> map){ //从请求体中获取用户名密码 String username = map.get(jwtProperties.getUserParamName()); String password = map.get(jwtProperties.getPwdParamName()); if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){ return buildErrorResponse("用户名或者密码不能为空"); } //根据用户名(用户Id)去数据库查找该用户 SysUser sysUser = sysUserRepository.findByUsername(username); if(sysUser != null){ //将数据库的加密密码与用户明文密码match boolean isAuthenticated = passwordEncoder.matches(password,sysUser.getPassword()); if(isAuthenticated){ //如果匹配成功 //通过jwtTokenUtil生成JWT令牌并return return buildSuccessResponse(jwtTokenUtil.generateToken(username,null)); } else{ //如果密码匹配失败 return buildErrorResponse("请确定您输入的用户名或密码是否正确!"); } }else{ return buildErrorResponse("请确定您输入的用户名或密码是否正确!"); } } /** * 刷新JWT令牌,用旧的令牌换新的令牌 */ @RequestMapping("/refreshtoken") public Mono<AjaxResponse> refreshtoken(@RequestHeader("${zimug.gateway.jwt.header}") String oldToken){ if(!jwtTokenUtil.isTokenExpired(oldToken)){ return buildSuccessResponse(jwtTokenUtil.refreshToken(oldToken)); } return Mono.empty(); } private Mono<AjaxResponse> buildErrorResponse(String message){ return Mono.create(callback -> callback.success( //请求结果成功的回调 AjaxResponse.error( //响应信息是Error的,携带异常信息返回 new CustomException(CustomExceptionType.USER_INPUT_ERROR, message) ) )); } private Mono<AjaxResponse> buildSuccessResponse(Object data){ return Mono.create(callback -> callback.success( //请求结果成功的回调 AjaxResponse.success(data) //成功响应,携带数据返回 )); } } ~~~ 四个核心服务代码类,后文会介绍 * JwtProperties,JWT配置加载类,包含JWT密钥配置、过期时间等参数配置 * SysUserRepository,数据库sys\_user表对应的JPA Repository。因为该表是用户信息表,包含用户名密码。 * JwtTokenUtil,JWT令牌操作工具封装类。核心方法如:根据用户id生成JWT令牌,校验令牌合法性,刷新令牌等工具类 * PasswordEncoder,是Spring Security的加解密工具类。核心方法是encode用于密码加密;matches用于密码校验。(在用户注册的时候用encode加密,在用户登录认证的时候用matches进行密码校验) ## 三、 JwtProperties 以下的这些配置属性,需要在gateway的配置文件中配置,不配置的话将使用默认值。 ~~~ @Data @ConfigurationProperties(prefix = "zimug.gateway.jwt") @Component public class JwtProperties { //是否开启JWT,即注入相关的类对象 private Boolean enabled; //JWT密钥 private String secret; //JWT有效时间 private Long expiration; //前端向后端传递JWT时使用HTTP的header名称,前后端要统一 private String header; //用户登录-用户名参数名称 private String userParamName = "username"; //用户登录-密码参数名称 private String pwdParamName = "password"; //是否使用默认的JWTAuthController private Boolean useDefaultController = false; } ~~~ ~~~ zimug: gateway: jwt: enabled: true #是否开启JWT登录认证功能 secret: fjkfaf;afa # JWT私钥,用于校验JWT令牌的合法性 expiration: 3600000 #JWT令牌的有效期,用于校验JWT令牌的合法性 header: JWTHeaderName #HTTP请求的Header名称,该Header作为参数传递JWT令牌 userParamName: username #用户登录认证用户名参数名称 pwdParamName: password #用户登录认证密码参数名称 useDefaultController: true # 是否使用默认的JwtAuthController ~~~ 这些配置在代码中会影响程序的组件加载及运行逻辑,比如:当ConditionalOnProperty---`zimug.gateway.jwt.useDefaultController=true`的时候,才初始化JwtAuthController 这个类的Bean。这样做的目的是,我规划的gateway未来不仅支持JWT还支持OAuth,为了避免二者冲突或者冗余。我们加上开关去影响Bean的初始化行为。 ![](https://img.kancloud.cn/92/d4/92d45f1bb4fdd05e75207fdc0924d79b_1327x141.png) ## 四、SysUserRepository SysUser 实体类对应数据库的sys\_user表,遵循JPA规则定义 ~~~ @Data @AllArgsConstructor @NoArgsConstructor @Entity @Table(name="sys_user") public class SysUser { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private String username; @Column private String password; @Column private Integer orgId; @Column private Boolean enabled; @Column private String phone; @Column private String email; @Column private Date createTime; } ~~~ 根据sys\_user表的username字段去查询SysUser用户信息。 ~~~ public interface SysUserRepository extends JpaRepository<SysUser,Long> { //注意这个方法的名称,jPA会根据方法名自动生成SQL执行,完全不用自己写SQL SysUser findByUsername(String username); } ~~~ 需要在配置文件中加入jpa及数据源相关的配置 ~~~ spring: datasource: url: jdbc:mysql://ip:3306/linnadb?useUnicode=true&characterEncoding=utf-8&useSSL=false username: password: driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: validate database: mysql show-sql: true ~~~ ## 五、PasswordEncoder 我们需要通过PasswordEncoder进行密码的解签名校验,所以初始化一个Bean:BCryptPasswordEncoder。需要注意的是:我们使用BCryptPasswordEncoder.matches解签名的前提是,用户注册的时候存放到数据库里面的password也是经过BCryptPasswordEncoder.encode加密的。 ![](https://img.kancloud.cn/35/e1/35e131ed9937da34300c241eaf0d385a_637x279.png) ## 六、JwtTokenUtil 基于io.jsonwebtoken-jjwt类库的代码封装,工具类。 ~~~ @Component public class JwtTokenUtil { @Resource private JwtProperties jwtProperties; /** * 生成token令牌 * * @param userId 用户Id或用户名 * @param payloads 令牌中携带的附加信息 * @return 令token牌 */ public String generateToken(String userId, Map<String,String> payloads) { int payloadSizes = payloads == null? 0 : payloads.size(); Map<String, Object> claims = new HashMap<>(payloadSizes + 2); claims.put("sub", userId); claims.put("created", new Date()); if(payloadSizes > 0){ for(Map.Entry<String,String> entry:payloads.entrySet()){ claims.put(entry.getKey(),entry.getValue()); } } return generateToken(claims); } /** * 从令牌中获取用户名 * * @param token 令牌 * @return 用户名 */ public String getUsernameFromToken(String token) { String username; try { Claims claims = getClaimsFromToken(token); username = claims.getSubject(); } catch (Exception e) { username = null; } return username; } /** * 判断令牌是否过期 * * @param token 令牌 * @return 是否过期 */ public Boolean isTokenExpired(String token) { try { Claims claims = getClaimsFromToken(token); Date expiration = claims.getExpiration(); return expiration.before(new Date()); } catch (Exception e) { //验证JWT签名失败等同于令牌过期 return true; } } /** * 刷新令牌 * * @param token 原令牌 * @return 新令牌 */ public String refreshToken(String token) { String refreshedToken; try { Claims claims = getClaimsFromToken(token); claims.put("created", new Date()); refreshedToken = generateToken(claims); } catch (Exception e) { refreshedToken = null; } return refreshedToken; } /** * 验证令牌 * * @param token 令牌 * @param userId 用户Id用户名 * @return 是否有效 */ public Boolean validateToken(String token, String userId) { String username = getUsernameFromToken(token); return (username.equals(userId) && !isTokenExpired(token)); } /** * 从claims生成令牌,如果看不懂就看谁调用它 * * @param claims 数据声明 * @return 令牌 */ private String generateToken(Map<String, Object> claims) { Date expirationDate = new Date(System.currentTimeMillis() + jwtProperties.getExpiration()); return Jwts.builder().setClaims(claims) .setExpiration(expirationDate) .signWith(SignatureAlgorithm.HS512, jwtProperties.getSecret()) .compact(); } /** * 从令牌中获取数据声明,验证JWT签名 * * @param token 令牌 * @return 数据声明 */ private Claims getClaimsFromToken(String token) { Claims claims; try { claims = Jwts.parser().setSigningKey(jwtProperties.getSecret()).parseClaimsJws(token).getBody(); } catch (Exception e) { claims = null; } return claims; } } ~~~ ## 七、访问测试 本机启动网关,进行`http://127.0.0.1:8777/authentication`登录认证,返回如下结果说明我们的实现是ok的。 ![](https://img.kancloud.cn/42/2d/422d96b3e4fa770a1c69de54eba7c410_1524x670.png) 测试令牌的刷新 ![](https://img.kancloud.cn/76/8b/768b5bab6308cc1ebf51f583d9b9eaff_1415x583.png)