开篇
阅读本文之前,首先需要了解 Spring security 的相关内容,最起码需要将我之前的系列文章看完。
其次需要学习 jwt(全称 JSON Web Token 的相关知识),关于 jwt推荐参考阮一峰老师的 JSON Web Token 入门教程,也可自行查阅其他资料。
正文
假设我们有一个前后端分离的项目, 前端直接去访问后端接口肯定会涉及接口访问权限问题.
通常的解决办法是:
客户端访问后端接口时,需要在请求头携带 token,然后后台通过拦截器对需要权限访问的接口, 去判断 token 的有效性, 最终决定 放行 还是 拦截 请求。
本文会基于 Spring Security + jwt 的方式去实现上述的小需求.
创建工程
将 上一篇文章 中的工程复制一份,然后做了些改动,这次改动的东西比较多(增加和删除了一些类,增加了一些配置),当然我会尽量讲清楚做了哪些改动,所以大家自行选择是新建工程还是复制一份。
引入 pom 依赖
较之上一篇的内容,本次新增了一个 jwt 的依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--JWT(Json Web Token)-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
修改 springboot 的 yml 配置文件
如果看了 jwt 的相关资料, 就能明白下面配置的含义, 如果还是不懂, 可以接着往下看。
jwt:
tokenHeader: Authorization # JWT存储的请求头
secret: mySecret # JWT加解密使用的密钥
expiration: 604800 # JWT的超期限时间(60*60*24)
tokenHead: Bearer # JWT负载中拿到开头
准备几个公共类
为了图方便,从别处 copy 的工具类,可以简单看下。
三个类合在一起的作用就是可以返回一个 json 格式的工具类。代码放在 common 包下面。
- CommonResult 响应工具类, 返回 json 数据给客户端。例如:
{code:200, message:"登录成功",data:null}
-
IErrorCode 接口,定义了两个方法
-
ResultCode 枚举类 实现了 IErrorCode 接口,定义响应返回结果
封装一个 jwt 的工具类
同样是从其他地方拷贝的一个 JwtTokenUtil 类,里面封装了一些方法,诸如:生成token,判断 token 的有效性等。无需纠结方法是如何实现的,只管拿来用.
定义登录接口
在 UserController 类里面定义一个登录方法,如下所示。
-
需要调用
UserDetailService的loadUserByUsername方法,如果看过我之前的文章,这里应该清楚这个方法主要做用户身份处理的。 -
调用 security 的 api,可以理解为security认证用户登录成功
SecurityContextHolder.getContext().setAuthentication(authentication)`
-
生成 token 信息返回给客户端,这个 token 是供客户端调用其他接口(需要用户登录才能调用的接口)时使用的。
/** * 用户登录接口 * @param username 用户名 * @param password 密码 * @return */ @PostMapping("/login") public Object login(@RequestParam("username") String username, @RequestParam("password") String password) { try { // 校验用户信息 UserDetails userDetails = userDetailsService.loadUserByUsername(username); // 保存用户登录态 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication);// 进行到这一步,security 才会认为你登陆成功 // 根据用户信息生成 token String token = jwtTokenUtil.generateToken(userDetails); return CommonResult.success(token);// 到这里, 从我们的业务角度来说登陆成功了 } catch (AuthenticationException e) { System.out.println("登录异常: " + e.getMessage()); return CommonResult.failed("登录失败"); }}
定义一个过滤器
过滤器的功能就是用来拦截请求的 url ,然后校验 token 的有效性。
SecurityContextHolder.getContext().setAuthentication(authentication);
这段代码着重说一下,可以简单理解为SecurityContextHolder.getContext() 为 Security 提供的一个保存已登录用户信息的一个容器.
通过调用它的 setAuthentication 以及 getAuthentication方法,可以将用户信息存入容器和从容器中取出。
/**
* JWT登录授权过滤器
*/
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
// 1. 从请求头中取出 token ,key随便定义,我这里定义在了配置文件中
String authHeader = request.getHeader(this.tokenHeader);
// 2. 判断是否为空,以及是否以 "Bearer " 开头 ps :固定格式
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
String authToken = authHeader.substring(this.tokenHead.length());// 必须以 "Bearer " 开头
// 3. 从 token 里获取用户名
String username = jwtTokenUtil.getUserNameFromToken(authToken);
// 4. 只要 token 没过期,就让用户保持登录状态 jwt 令牌只是辅助登录, 真正是否登录要看 authentication 是否有效
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 下面这段代码就是为了让 security 保持用户登录状态, 在UserController 里你也可以看到
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 核心代码
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
将 JwtFilter 添加到 Spring Security 中
在 WebSecurityConfig 配置类的 configure 方法中的最后一行加上如下代码。该方法中的其他代码的含义,不懂得可以参考前面写的博客。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();// 必须有, 不然会 403 forbidden
/*
http.formLogin()
.loginPage("/loginPage.html")// 自定义登录页
.loginProcessingUrl("/form/login")// 自定义登录 action, 名字随便起
.successHandler(successHandler)// 自定义登录成功处理类
.failureHandler(failureHandler);// 自定义登录失败处理类
*/
// 访问 "/form/login", "/loginPage.html" 放行
http.authorizeRequests().antMatchers("/user/userInfo", "/user/login", "/form/login", "/loginPage.html").permitAll()
.antMatchers("/hello").hasRole("superadmin") // 只有superadmin 角色的用户才能访问
.anyRequest().authenticated();
/*http.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)// 用户没有访问权限处理器
.authenticationEntryPoint(entryPoint);// 用户没有登录处理器*/
// 这里是新增的代码, 添加拦截器
http.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
总结
至此 Sprng Securiy 系列实战博客更新完毕, 希望帮助到需要的人,也欢迎大家给出意见和批评。
同时想多说一嘴,这一系列实战文章并没有涉及 Spring Security 的工作原理,全程都在讲如何使用。
比如下面这段代码,只是讲了这段代码的逻辑,并没有讲清楚为什么这样用。这些是后面原理文章中会讲解的内容。
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}