权限与认证:基于 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 db
create 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.xml
debug: true
server:
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
@NoArgsConstructor
public 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;
}
}
为了保证能够有效拦截,我们还需要做一些配置:
@Configuration
public 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 的用户登录授权的完整示例就介绍完毕了。
版权声明: 本文为 InfoQ 作者【程序员架构进阶】的原创文章。
原文链接:【http://xie.infoq.cn/article/b3875a66d792136f387f4d3df】。文章转载请联系作者。
程序员架构进阶
磨炼中成长,痛苦中前行 2017.10.22 加入
微信公众号【程序员架构进阶】。多年项目实践,架构设计经验。曲折中向前,分享经验和教训
评论