当我们使用SpringBoot实现了一个简单的API接口之后,我们如何去保证我们的API接口只让我们运行的人调用呢。这时候就需要对我们的API接口进行保护。在别人访问这些接口的时候,我们对访问者进行身份的验证,从而对接口的保护。
基本流程如下图:
当然我们需要一个接口给用户请求Token,要不然用户拿不到token怎么去请求其他资源呢。流程图如下:
接下来我们按照流程一步一步实现。
首先实现请求token
@GetMapping("/get_token")
public JsonResult getToken(@RequestParam String username,@RequestParam String password){
//apiUser通过用户url传入的账号和密码
ApiUser apiUser=loginService.loginApiUser(username);
//这里验证用户的信息是否正确
JsonResult jsonResult=checkApiUser(apiUser,password);
//如果正确那么返回Null,不正确返回提示然后显示给用户
if (jsonResult!=null){
return jsonResult;
}
//到这里证明用户信息是正确的,那么我们进行token的生成然后返回给用户
String token=loginService.generateToken(apiUser);
return JsonResult.suc(token);
}
- 通过url拿到用户传过来的数据,账户和密码
- 对账户密码进行验证,如果不正确返回错误提示。
- 如果正确进行token生存,然后返回给用户
那么我们如何验证呢
private JsonResult checkApiUser(ApiUser apiUser,String password){
if (apiUser==null){
return JsonResult.error(434,"账户不存在");
}else {
if (apiUser.getEnable()==0){
return JsonResult.error(452,"账户在黑名单");
}
if (!apiUser.getPassword().equals(password)){
//equals相等返回true
return JsonResult.error(452,"账户密码错误");
}
}
return null;
}
- 先通过用户传入的用户名进行查找,看是否存在该用户。
- 如果存在该用户,判断用户的状态是否可用。
- 如果状态可用,那么比较数据库保存的密码和用户输入的密码是否匹配。
那么如何生成token呢
public String generateToken(ApiUser tokenDetail) {
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("sub", tokenDetail.getUsername());
claims.put("created", this.generateCurrentDate());
return this.generateToken(claims);
}
/**
* 根据 claims 生成 Token
*
* @param claims
* @return
*/
private String generateToken(Map<String, Object> claims) {
logger.info("成功进入生产token",claims);
try {
return Jwts.builder()
.setClaims(claims)
.setExpiration(this.generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, this.secret.getBytes("UTF-8"))
.compact();
} catch (UnsupportedEncodingException ex) {
//didn't want to have this method throw the exception, would rather log it and sign the token like it was before
logger.warn(ex.getMessage());
return Jwts.builder()
.setClaims(claims)
.setExpiration(this.generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, this.secret)
.compact();
}
}
- 我们通过JWT工具去进行token的生成。(如果不了解JWT可用百度看看,非常简单。)
- 通过用户信息,我们生成一个token并返回给客户端。
- 用户拿着这个token去请求资源,就可以了。
接下来我们看用户请求资源是如何实现的
@PostMapping("/courseList")
public JsonResult getAllCourse(Page<Course> page, Integer state){
//获取当前学年的课程列表
return JsonResult.suc(courseService.getAll(page,state));
}
这是一个资源API接口,那么用户在访问这个接口的时候我们就需要对身份进行验证,那么是在哪里验证的呢
@Configuration // 声明为配置类
@EnableWebSecurity // 启用 Spring Security web 安全的功能
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 注册 401 处理器
*/
@Autowired
private EntryPointUnauthorizedHandler unauthorizedHandler;
/**
* 注册 403 处理器
*/
@Autowired
private MyAccessDeniedHandler accessDeniedHandler;
/**
* 注册 token 转换拦截器为 bean
* 如果客户端传来了 token ,那么通过拦截器解析 token 赋予用户权限
*
* @return
* @throws Exception
*/
@Bean
public AuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
AuthenticationTokenFilter authenticationTokenFilter = new AuthenticationTokenFilter();
authenticationTokenFilter.setAuthenticationManager(authenticationManagerBean());
return authenticationTokenFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/get_token").permitAll() // 所有人可以访问
.anyRequest().authenticated() // 必须携带token
.and()
// 配置被拦截时的处理
.exceptionHandling()
.authenticationEntryPoint(this.unauthorizedHandler) // 添加 token 无效或者没有携带 token 时的处理
.accessDeniedHandler(this.accessDeniedHandler) //添加无权限时的处理
.and()
.csrf()
.disable() // 禁用 Spring Security 自带的跨域处理
.sessionManagement() // 定制我们自己的 session 策略
.sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 调整为让 Spring Security 不创建和使用 session
/**
* 本次 json web token 权限控制的核心配置部分
* 在 Spring Security 开始判断本次会话是否有权限时的前一瞬间
* 通过添加过滤器将 token 解析,将用户所有的权限写入本次会话
*/
http
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
}
}
- 首先我们通过SpringSecurity进行url配置,也就是给URL加了过滤器。
- 当用户访问需要token的资源路径的时候就会触发过滤器。
-
http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);这一句相当于把我们自定义的过滤器加入到Springsecurity过滤器链中。
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 将 ServletRequest 转换为 HttpServletRequest 才能拿到请求头中的 token
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 尝试获取请求头的 token
String authToken =httpRequest.getHeader(this.tokenHeader);//获取token=xxx
// 尝试拿 token 中的 username
// 若是没有 token 或者拿 username 时出现异常,那么 username 为 null
String username = this.tokenUtils.getUsernameFromToken(authToken);
// 如果上面解析 token 成功并且拿到了 username 并且本次会话的权限还未被写入
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 用 UserDetailsService 从数据库中拿到用户的 UserDetails 类
// UserDetails 类是 Spring Security 用于保存用户权限的实体类
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 检查用户带来的 token 是否有效
// 包括 token 和 userDetails 中用户名是否一样, token 是否过期, token 生成时间是否在最后一次密码修改时间之前
// 若是检查通过
if (this.tokenUtils.validateToken(authToken, userDetails)) {
// 生成通过认证
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));
// 将权限写入本次会话
SecurityContextHolder.getContext().setAuthentication(authentication);
}
if (!userDetails.isEnabled()){
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print("{\"code\":\"452\",\"data\":\"\",\"message\":\"账号处于黑名单\"}");
return;
}
}
chain.doFilter(request, response);
}
- 在自定义过滤器中通过 String authToken =httpRequest.getHeader(this.tokenHeader);这一句代码我们获得了用户传入的token值。
- 通过对token值的解析我们可以获取到用户信息,这里的信息在是在生成token的时候我们加入到token中的。(获取到用户信息你可以对用户操作进行一些记录。)同时如果获取的用户信息不存在我们数据库中,那么证明该token不是正确的,不在进行后面的业务逻辑
- 用户信息正确,我们对token进行验证看token是否过期,或者用户是否已经被禁用了。
this.tokenUtils.validateToken(authToken, userDetails)这一句进行验证
这样完整的鉴权流程我们就实现了。上面只是部分代码。
源码:https://github.com/xushuoAI/Springboot-SpringSecurity-Mybatis-Redis-
以上代码参考了许多博主的博文。非常感谢各位前辈的分享。