写点什么

权限与认证:基于 JWT 的授权实现

发布于: 2021 年 06 月 05 日
权限与认证:基于JWT的授权实现

系列文章:

权限与认证:JWT

权限与认证:JWT 实践


一 摘要

权限与认证:JWT 实践给出了一个简单的 JWT 应用示例,本篇将实现一个登录授权的 demo,来了解 JWT 在用户登录验证时的使用。

二 操作步骤

2.1 创建工程

我们还是创建一个 maven 工程,引入 springboot,以及 jwt 相关依赖。另外,因为是用户登录验证场景,所以还需要一张表存储用户信息。所以,还会有 mysql 相关依赖。pom.xml 的依赖内容如下:


<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency>
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
复制代码

2.2 建立用户表

因为是示例,所以这里不需要过多信息,建表 sql 如下:

SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0;
-- 创建auth dbcreate database auth;use auth;
DROP TABLE IF EXISTS `user`;CREATE TABLE `user` ( `id` int(45) NOT NULL, `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ------------------------------ Records of user-- ----------------------------INSERT INTO `user` VALUES (1, '张三', '123456');
SET FOREIGN_KEY_CHECKS = 1;
复制代码

2.3 orm 框架

有数据库,也会有对象-关系映射。常用的有 hibernate 和 ibatis/mybatis,本示例中使用 mybatis。

需要几个配置文件:

1、application.yaml

配置数据库连接信息:

spring:  datasource:    driver-class-name: com.mysql.jdbc.Driver    url: jdbc:mysql://localhost:3306/auth?useSSL=false    username: root    password:mybatis:  config-location: classpath:mybatis.xmldebug: trueserver:  port: 8888
复制代码

2、mybatis 相关配置(非必须)

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration>    <settings>        <setting name="logImpl" value="SLF4J"/>        <!-- 开启驼峰命名转换 Table(create_time) -> Entity(createTime) -->        <setting name="mapUnderscoreToCamelCase" value="true" />    </settings></configuration>
复制代码

2.4 业务代码结构


三 核心代码说明

3.1 orm 业务配置

与前面的 mybatis 配置不同,这里是配置 mapper,也就是业务操作的 sql 语句,以及 Mapper 接口。

UserMapper.xml:

<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" ><mapper namespace="com.flamingskys.auth.jwt.mapper.UserMapper">    <select id="findByUsername" resultType="com.flamingskys.auth.jwt.entity.User">      SELECT * FROM user      where      username=#{username}    </select>    <select id="findUserById" resultType="com.flamingskys.auth.jwt.entity.User">        SELECT * FROM user        where id=#{Id}    </select></mapper>
复制代码

为了查询用户信息,这里填写了两个查询 sql,分别是基于 ID 和 基于 userName 查询用户信息的。

UserMapper.java

package com.flamingskys.auth.jwt.mapper;
import com.flamingskys.auth.jwt.entity.User;import org.apache.ibatis.annotations.Param;
/** * @author flamingskys * @date 2021-05-30 22:37 */public interface UserMapper { User findByUsername(@Param("username") String username);
User findUserById(@Param("Id") String Id);}
复制代码

3.2 实体类

显然,核心实体就是用户信息,User.java:

@ToString@Data@AllArgsConstructor@NoArgsConstructorpublic class User {    Integer Id;    String username;    String password;}
复制代码

3.3 Service

会包括两个 service。一个是用户相关操作的,底层使用 UserMapper;另一个,就是关系到 JWT 的 token 生成 service。

UserService.java:

@Service("UserService")public class UserService {
@Autowired UserMapper userMapper;
public User findByUsername(String userName){ return userMapper.findByUsername(userName); }
public User findUserById(String userId) { return userMapper.findUserById(userId); }
}
复制代码

TokenService.java

@Service("TokenService")public class TokenService {
public String getToken(User user) { String token=""; token= JWT.create().withAudience(String.valueOf(user.getId()))// 将 user id 保存到 token 里面 .sign(Algorithm.HMAC256(user.getPassword()));// 以 password 作为 token 的密钥 return token; }}
复制代码

如果还记得上一篇的内容,会发现跟上面的示例是一样的。没错,那个简单示例就是摘取这部分的逻辑。

3.4 Controller

接下来就是 MVC 中的控制层,controller 了。这里只需要一个 controller,用于登录的:

@Slf4j@RestController@RequestMapping("api")public class LoginController {    @Autowired    UserService userService;    @Autowired    TokenService tokenService;
@PostMapping("/login") public Object login(LoginRequest loginRequest){ JSONObject jsonObject=new JSONObject(); User userForBase=userService.findByUsername(loginRequest.getUsername());
if(userForBase==null){ jsonObject.put("message","登录失败,用户不存在"); return jsonObject; }else { if (!userForBase.getPassword().equals(loginRequest.getPassword())){ jsonObject.put("message","登录失败,密码错误"); return jsonObject; }else { String token = tokenService.getToken(userForBase); jsonObject.put("token", token); jsonObject.put("user", userForBase); return jsonObject; } } } @UserLoginToken @GetMapping("/getMessage") public String getMessage(AuthenUser authenUser){ log.info("userInfo:{}", authenUser.toString()); return "当前登录的用户姓名:"+authenUser.toString(); }}
复制代码

里面包含了两个接口,一个处理登录逻辑,另一个查询用户信息。

等等,getMessage()这个借口,有一个 AuthenUser 参数,并且在方法内直接打印用户信息。这个是哪里来的?

3.5 用户登录拦截

为了保证每次请求 getMessage 接口(这就是一个访问系统资源的操作)的用户,都是被授权了的,那么我们就需要借助拦截器。在访问这类操作前,验证身份信息,如果没有被授权,就跳转到登录页面。这是通过拦截器来实现的:

public class AuthenticationInterceptor implements HandlerInterceptor {    private static final Logger logger = LoggerFactory.getLogger(TestUserArgumentResolver.class);
@Autowired UserService userService;
@Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception { String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token if(!(object instanceof HandlerMethod)){ return true; } HandlerMethod handlerMethod=(HandlerMethod)object; Method method=handlerMethod.getMethod(); //检查是否有passtoken注释,有则跳过认证// if (method.isAnnotationPresent(PassToken.class)) {// PassToken passToken = method.getAnnotation(PassToken.class);// if (passToken.required()) {// return true;// }// } //检查有没有需要用户权限的注解 if (method.isAnnotationPresent(UserLoginToken.class)) { UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class); if (userLoginToken.required()) { // 执行认证 if (token == null) { throw new RuntimeException("无token,请重新登录"); } // 获取 token 中的 user id String userId; try { userId = JWT.decode(token).getAudience().get(0); } catch (JWTDecodeException j) { throw new RuntimeException("401"); } User user = userService.findUserById(userId); if (user == null) { throw new RuntimeException("用户不存在,请重新登录"); } // 验证 token JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build(); try { jwtVerifier.verify(token); } catch (JWTVerificationException e) { throw new RuntimeException("401"); }
//首次通过校验后,设置到header AuthenUser authenUser = new AuthenUser(); authenUser.setId(user.getId()); authenUser.setUserName(user.getUsername());
httpServletRequest.setAttribute(AuthenUser.class.getName(), authenUser); return true; } } return true; }
}
复制代码

为了保证能够有效拦截,我们还需要做一些配置:

@Configurationpublic class WebConfig implements WebMvcConfigurer {
@Autowired private AuthenticationInterceptor authenticationInterceptor;
@Autowired private TestUserArgumentResolver testUserArgumentResolver;
@Override public void addInterceptors(InterceptorRegistry registry) {
List<String> authUserExcludePathPatterns = new ArrayList<>(); authUserExcludePathPatterns.add("/login");
registry.addInterceptor(qkdAdminUserInterceptor).addPathPatterns("/**") .excludePathPatterns(authUserExcludePathPatterns);
}
@Bean public AuthenticationInterceptor authenticationInterceptor() { return new AuthenticationInterceptor(); }
@Bean public TestUserArgumentResolver testUserArgumentResolver() { return new TestUserArgumentResolver(); }

@Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { argumentResolvers.add(testUserArgumentResolver()); }}
复制代码

通过拦截器配置,来实现除了 login 接口本身之外,其他接口都需要授权后才能访问。

3.6 用户信息注入参数

上面的配置中,会发现还有一个非拦截器的类。这个类是用于实现用户信息参数的注入。我们在得到授权后,请求资源接口时,常常会有根据用户 id 或者角色权限等去获取有限制的资源。那么怎样快速使用用户信息呢?UserArgumentResolver 就是为了实现这个。

public class UserArgumentResolver implements HandlerMethodArgumentResolver {    private static final Logger logger = LoggerFactory.getLogger(UserArgumentResolver.class);
//只解析方法中参数类型是AuthenUser的参数 public boolean supportsParameter(MethodParameter parameter) { Class<?> clazz = parameter.getParameterType(); return clazz== AuthenUser.class; }
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
AuthenUser result = null;
logger.error("header:"+request.getAttribute(AuthenUser.class.getName()));
try { Object obj = request.getAttribute(AuthenUser.class.getName()); if (obj != null && obj instanceof AuthenUser) { result = (AuthenUser)obj; } } catch (Exception var8) { logger.error("resolve argument failed", var8); }
return result; }
}
复制代码

后面我们就可以在接口方法中,直接加上 AuthenUser user 参数,并在方法内直接使用用户信息。这样方便很多。

3.7 启动类

最后别忘了程序启动入口,有了它,我们就可以启动工程,进行接口验证了。使用 postman 就可以,也可以自己写一个简单的登录界面。

@SpringBootApplication@MapperScan("com.flamingskys.auth.jwt.mapper")public class AuthJwtApplication {
public static void main(String[] args) { SpringApplication.run(AuthJwtApplication.class, args); }}
复制代码


至此,一个使用 JWT 的用户登录授权的完整示例就介绍完毕了。

发布于: 2021 年 06 月 05 日阅读数: 13
用户头像

磨炼中成长,痛苦中前行 2017.10.22 加入

微信公众号【程序员架构进阶】。多年项目实践,架构设计经验。曲折中向前,分享经验和教训

评论

发布
暂无评论
权限与认证:基于JWT的授权实现