JWT基本概念

JWT,即 JSON Web Tokens(RFC 7519),是一个广泛用于验证 REST APIs 的标准。虽说是一个新兴技术,但它却得以迅速流行。

  • JWT的验证过程是:

    • 前端(客户端)首先发送一些凭证来登录(我们编写的是 web 应用,所以这里使用用户名和密码来做验证)。

    • 后端(服务端)这里指Spring应用校验这些凭证,如果校验通过则生成并返回一个 JWT。

    • 客户端需要在请求头的Authorization字段中以 “Bearer TOKEN” 的形式携带获取到的token,服务端会检查这个token是否可用并决定授权访问或拒绝请求。

      • token中可能保存了用户的角色信息,服务端可以根据用户角色来确定访问权限。

实现

我们来看一下在实际的 Spring 项目中是如何实现JWT登录和保存机制的。

依赖

下面是我们示例代码的 Maven 依赖列表,注意,截图中并未包含Spring Boot、Hibernate等核心依赖(你需要自行添加)。

用户模型

  • 创建一个包含保存用户信息、基于用户名和密码验证用户权限功能的 controller。
  • 创建一个名为 User 的实体类,它是数据库中 USER 表的映射。需要的话,可以在其中添加其他属性。

  • 还需要定义一个 UserRepository 类来保存用户信息,重写其 findByUsername 方法,在验证过程中会用到。
public interface UserRepository extends JpaRepository<User, String>{
User findByUsername(String username);
}
  • 千万不能在数据库中保存明文密码,因为很多用户喜欢在各种网站上使用相同的密码。

  • 哈希算法有很多,BCrypt是最常用的之一,它也是推荐用于安全加密的算法。关于这个话题的更多内容,可以查看 这篇文章

为了加密密码,我们在 @bean 注解标记的主类中定义一个 BCrypt Bean,如下所示:

@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}

加密密码的时候将会调用这个Bean里面的方法。

  • 创建一个名为 UserController 的类,为其添加 @RestController 注解并定义路由映射。

  • 在这个应用中,我们接收前端传入的 UserDto 对象来保存用户信息。你也可以选择在 @RequestBody 参数中接收 User 对象。

@RestController
@RequestMapping("/api/services/controller/user")
@AllArgsConstructor
public class UserController {
private UserService userService;
@PostMapping()
public ResponseEntity<String> saveUser(@RequestBody UserDto userDto) {
return new ResponseEntity<>(userService.saveDto(userDto), HttpStatus.OK);
}
}

我们使用之前定义的 BCrypt Bean 来加密传入的 UserDto 对象的 password 字段。这个操作也可以在 controller 之中执行,但是把逻辑操作集中到 service 类中是更好的做法。

@Transactional(rollbackFor = Exception.class)
public String saveDto(UserDto userDto) {
userDto.setPassword(bCryptPasswordEncoder.encode(userDto.getPassword()));
return save(new User(userDto)).getId();
}

验证过滤器

需要通过权限验证来确定用户的真实身份。这里我们使用经典的【用户名-密码对】的形式来完成。

验证步骤:

  • 创建继承 UsernamePasswordAuthenticationFilter 的验证过滤器
  • 创建继承 WebSecurityConfigurerAdapter 的安全配置类并应用过滤器
  • 验证过滤器的代码如下——也许你已经知道了,过滤器是 Spring Security 的核心。
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
setFilterProcessesUrl("/api/services/controller/user/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException {
try {
User creds = new ObjectMapper().readValue(req.getInputStream(), User.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) throws IOException {
String token = JWT.create()
.withSubject(((User) auth.getPrincipal()).getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.sign(Algorithm.HMAC512(SECRET.getBytes()));
String body = ((User) auth.getPrincipal()).getUsername() + " " + token;
res.getWriter().write(body);
res.getWriter().flush();
}
}
  • Spring Security 默认使用继承了 UsernamePasswordAuthenticationFilter 的子类进行密码验证 ,我们可以在其中编写自定义的验证逻辑。

    • 我们在构造函数中调用setFilterProcessesUrl 方法,设置默认登录地址。

    • 如果删除这行代码,Spring Security 会生成一个默认的 “/login” 端点,我们可以不用在 controller 中显式地定义登录端点。

    • 这行代码执行之后,我们的登录端点将被设置为 /api/services/controller/user/login,你可以根据自己的实际代码来设置。

  • 我们重写了 UsernameAuthenticationFilter 类的 attemptAuthentication 和 successfulAuthentication 方法。

    • 用户登录时会执行 attemptAuthentication方法,它会读取凭证信息、创建用户 POJO、校验凭证并授权。

      • 我们传入用户名、密码以及一个空列表。我们还没有定义用户角色,所以把这个表示用户权限(角色)的列表留空就行。
  • 如果验证成功,就会执行 successfulAuthentication 方法,它的参数由Spring Security自动注入。

    • attemptAuthentication返回Authentication对象,这个对象包含了我们传入的权限信息。

    • 我们想在验证成功之后返回一个使用用户名、密钥和过期时间创建的 token。先定义SECRET和 EXPIRATION_DATE。


public class SecurityConstants {
public static final String SECRET = "SECRET_KEY";
public static final long EXPIRATION_TIME = 900_000; // 15 mins
public static final String TOKEN_PREFIX = "Bearer ";
public static final String HEADER_STRING = "Authorization";
public static final String SIGN_UP_URL = "/api/services/controller/user";
}
  • 创建一个类作为常量的容器,SECRET 的值可以任意设置,最佳的做法是在 hash 算法支持的范围内使用尽可能长的字符串。例如我们使用的是 HS256 算法,SECRET 字符串的最佳长度即为 256 bits/32 个字符。

  • 超时时间设置为 15 分钟,这是防御暴力破解密码的最佳实践。此处使用的时间单位为毫秒。

  • 验证过滤器准备好了,但还不可用,我们还要创建一个授权过滤器,再通过一个配置类来应用它们。

  • 授权过滤器会校验 Authorization 请求头中的 token 是否存在及其可用性。在配置类中指明哪些端点需要使用这个过滤器。

授权过滤器

  • doFilterInternal 方法拦截请求并校验 Authorization 请求头,如果不存在或者它的值不是以 “BEARER” 开头,则直接转到下一个过滤器。

  • 如果这个请求头携带了合法的值,会调用 getAuthentication 方法,校验这个 JWT,如果这个 token 是可用的,它会返回一个Spring内部使用的 token。

  • 这个新生成的 token 会被保存在 SecurityContext 中,如果需要基于用户角色进行授权的话,可以向这个 token 传入用户权限。

过滤器都准备好了,现在要通过配置类把它们投入使用。

public class JWTAuthorizationFilter extends BasicAuthenticationFilter {

    public JWTAuthorizationFilter(AuthenticationManager authManager) {
super(authManager);
} @Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws IOException, ServletException {
String header = req.getHeader(HEADER_STRING);
if (header == null || !header.startsWith(TOKEN_PREFIX)) {
chain.doFilter(req, res);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(req);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(req, res);
}
// Reads the JWT from the Authorization header, and then uses JWT to validate the token
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader(HEADER_STRING);
if (token != null) {
// parse the token.
String user = JWT.require(Algorithm.HMAC512(SECRET.getBytes()))
.build()
.verify(token.replace(TOKEN_PREFIX, ""))
.getSubject();
if (user != null) {
// new arraylist means authorities
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
return null;
}

配置处理

  • 给这个类添加 @EnableWebSecurity 注解,同时让它继承 WebSecurityConfigureAdapter 并实现自定义的安全逻辑。

  • 自动注入之前定义的 BCrypt Bean,同时自动注入 UserDetailsService 用来获取用户账户信息。

  • 最重要的是那个接收一个 HttpSecurity 对象作为参数的方法,其中声明了如何在各个端点中应用过滤器、配置了 CORS、放行了所有对注册接口的 POST 请求。

  • 可以添加其他匹配器来基于 URL 模式和角色进行过滤,你也可以 查看 StackOverflow 上这个问题的相关示例。另一个方法配置了 AuthenticationManager 在登录校验时使用我们指定的编码器。

@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter { private UserDetailsServiceImpl userDetailsService;
private BCryptPasswordEncoder bCryptPasswordEncoder; public WebSecurity(UserDetailsServiceImpl userService, BCryptPasswordEncoder bCryptPasswordEncoder) {
this.userDetailsService = userService;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
} @Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().authorizeRequests()
.antMatchers(HttpMethod.POST, SIGN_UP_URL).permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// this disables session creation on Spring Security
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
} @Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
} @Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
}

测试实现

发送一些请求来测试应用是否正常工作。

  • 使用 GET 请求访问受保护的资源,服务端返回了 403 状态码。

  • 这是程序设计预期的行为,因为我们没有在请求头中携带 token 信息。

现在创建一个用户:

发送一个携带了用户信息数据的 POST 请求,以创建用户。稍后将登陆这个账户来获取 token。

获取到 token 了,现在可以用这个 token 来访问受保护的资源。

在 Authorization 请求头中携带 token,就可以访问受保护的端点了。

总结

Spring 中实现 JWT 授权和密码认证的步骤,同时学习了如何安全地保存用户信息。

参考内容

How to Set Up Java Spring Boot JWT Authorization and

【SpringBoot技术专题】「JWT技术专区」SpringSecurity整合JWT授权和认证实现的更多相关文章

  1. 【SpringBoot技术专题】「权限校验专区」Shiro整合JWT授权和认证实现

    本章介绍一下常用的认证框架Shiro结合springboot以及集合jwt快速带您开发完成一个认证框架机制. Maven配置依赖 <dependency> <groupId>o ...

  2. SpringSecurity权限管理系统实战—六、SpringSecurity整合jwt

    目录 SpringSecurity权限管理系统实战-一.项目简介和开发环境准备 SpringSecurity权限管理系统实战-二.日志.接口文档等实现 SpringSecurity权限管理系统实战-三 ...

  3. SpringSecurity 整合 JWT

    项目集成Spring Security(一) 在上一篇基础上继续集成 JWT ,实现用户身份验证. 前言 前后端分离项目中,如果直接把 API 接口对外开放,我们知道这样风险是很大的,所以在上一篇中我 ...

  4. SpringSecurity整合JWT

    一.前言 最近负责支付宝小程序后端项目设计,这里主要分享一下用户会话.接口鉴权的设计.参考过微信小程序后端的设计,会话需要依靠redis.相关的开发人员和我说依靠Redis并不是很靠谱,redis在业 ...

  5. 「编程羽录」上线,程序员必备的这些技能你能get到嘛?

    大家好,我是小羽. 好久不见,给大家带来个好消息,小羽的全新专题「编程羽录」系列正式上新,主要是介绍一些关于面试题和经验总结的文章. 会为大家提供一些技术栈之外,程序员还需要的其他方面硬核知识,做到全 ...

  6. 【Java技术专题】「性能优化系列」针对Java对象压缩及序列化技术的探索之路

    序列化和反序列化 序列化就是指把对象转换为字节码: 对象传递和保存时,保证对象的完整性和可传递性.把对象转换为有字节码,以便在网络上传输或保存在本地文件中: 反序列化就是指把字节码恢复为对象: 根据字 ...

  7. 报名|「OneAPM x DaoCloud」技术公开课:Docker性能监控!

    如今,越来越多的公司开始 Docker 了,「三分之二的公司在尝试了 Docker 后最终使用了它」,也就是说 Docker 的转化率达到了 67%,同时转化时长也控制在 60 天内. 既然 Dock ...

  8. Linux 小知识翻译 - 「虚拟化技术 续」

    这次,继续聊聊「虚拟化技术」. 根据上回的介绍,虚拟化技术可以使「计算机的台数和运行的OS的个数的比例不再是1:1」.这回介绍一下如何使用这个技术. 使用方法之一,「一台计算机上运行多个OS」.从个人 ...

  9. Linux 小知识翻译 - 「虚拟化技术」

    这次聊聊「虚拟化技术」. 虚拟化技术,有时简称为「虚拟化」,最近经常听人说它.但是却不太清楚它的意思.到底虚拟了什么东西?本来是用来干什么的? 有名的虚拟化软件要数 VMware 和 VirtualB ...

随机推荐

  1. 8、基本数据类型(dict)

    8.1.字典: 1.字典元素用大括号括起来,用逗号分割每个元素,字典元素是"key:value"的形式 dic = { "k1": 'v1', #键值对 &qu ...

  2. web自动化之windows页面切换

    一.为什么切换windows页面 在页面操作过程中,存在点击某个元素之后会重新打开一个windows页面,如果不切换至新页面的话,无法在新页面中进行操作,程序会出现报错 二.如何切换 1.获取当前所有 ...

  3. 企业管理CRM不只是客户录入系统

    企业在举办营销活动或者展会之后,将会收集到大量的客户信息,将这些信息有效地整理.完善.储存也是一个不小的工程.如果您的企业经常面遇到这样的情况,不妨使用Zoho CRM系统来帮您完成.但是,Zoho ...

  4. webpack(6)webpack处理图片

    图片处理url-loader(webpack5之前的处理方式) 在项目开发中,我们时长会需要使用到图片,比如在img文件夹中有图片test1.png,然后在normal.css中会引用到图片 body ...

  5. MySql:MySql忘记密码怎么修改?

    1. 关闭正在运行的MySQL服务2. 打开DOS窗口,转到mysql\bin目录3. 输入mysqld --skip-grant-tables 回车       --skip-grant-table ...

  6. EF Core3.1 CodeFirst动态自动添加表和字段的描述信息

    前言 我又来啦.. 本篇主要记录如何针对CodeFirst做自动添加描述的扩展 为什么要用这个呢.. 因为EF Core3.1 CodeFirst 对于自动添加描述这块 只有少部分的数据库支持.. 然 ...

  7. jconsole和jstack

    1.jconsole jconsole是jdk自带的一个可视化的jvm监控工具,可以监控本地的jvm,也可以监控远程jvm 打开本地jdk安装目录下的bin目录下的jconsole.exe 2.jst ...

  8. C语言:FILE p和FILE *p

    FILE p和FILE *p大概可以这么理解:1 . 前一个p指文件型变量,后一个p指文件地址型变量.2 . 前一个p的内存地址已定,后一个p内存地址未定. 前一个是声明类对象,后一个是声明一个可指向 ...

  9. ls仅列出当前目录下的所有目录

    ls -d */ -d仅列出目录本身,而不列出其中的内容 *通配符,所有的字符 /目录的标识

  10. FPGA经典:Verilog传奇与基于FPGA的数字图像处理原理及应用

    一 简述 最近恶补基础知识,借了<<Verilog传奇>>,<基于FPGA的嵌入式图像处理系统设计>和<<基千FPGA的数字图像处理原理及应用>& ...