当前位置 : 主页 > 编程语言 > java >

SpringBoot集成Spring Security用JWT令牌实现登录和鉴权

来源:互联网 收集:自由互联 发布时间:2021-04-30
最近在做项目的过程中 需要用JWT做登录和鉴权 查了很多资料 都不甚详细 有的是需要在application.yml里进行jwt的配置 但我在导包后并没有相应的配置项 因而并不适用 在踩过很多坑之后

最近在做项目的过程中 需要用JWT做登录和鉴权 查了很多资料 都不甚详细
有的是需要在application.yml里进行jwt的配置 但我在导包后并没有相应的配置项 因而并不适用
在踩过很多坑之后 稍微整理了一下 做个笔记

一、概念

1、什么是JWT

Json Web Token (JWT)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)
该token被设计为紧凑且安全的 特别适用于分布式站点的单点登录(SSO)场景

随着JWT的出现 使得校验方式更加简单便捷化
JWT实际上就是一个字符串 它由三部分组成:头部 载荷和签名
用[.]分隔这三个部分 最终的格式类似于:xxxx.xxxx.xxxx

在服务器直接根据token取出保存的用户信息 即可对token的可用性进行校验 使得单点登录更为简单

2、JWT校验的过程

1、浏览器发送用户名和密码 发起登录请求
2、服务端验证身份 根据算法将用户标识符打包生成token字符串 并且返回给浏览器
3、当浏览器需要发起请求时 将token一起发送给服务器
4、服务器发现数据中携带有token 随即进行解密和鉴权
5、校验成功 服务器返回请求的数据

二、使用

1、首先是导包

<!-- Spring Security -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- Spring Security和JWT整合 -->
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-jwt</artifactId>
	<version>1.0.10.RELEASE</version>
</dependency>

<!-- JWT -->
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt</artifactId>
	<version>0.9.1</version>
</dependency>

<!-- 字符串转换需要用到此包 -->
<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-lang3</artifactId>
	<version>3.4</version>
</dependency>

2、实体类

两个实体类 一个是用户 另一个是权限

public class User {
  private Integer id;
  private String username;
  private String password;
  
	省略gettersetter之类的代码...
}
public class Role {
  private Integer id;
  private String username;
  private String name;
  
	省略gettersetter之类的代码...
}

3、然后需要一个Utils工具类

该类用于进行Token的加密和解密 可在此类中单元测试

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtTokenUtil {
  // Token请求头
  public static final String TOKEN_HEADER = "Authorization";
  // Token前缀
  public static final String TOKEN_PREFIX = "Bearer ";

  // 签名主题
  public static final String SUBJECT = "piconjo";
  // 过期时间
  public static final long EXPIRITION = 1000 * 24 * 60 * 60 * 7;
  // 应用密钥
  public static final String APPSECRET_KEY = "piconjo_secret";
  // 角色权限声明
  private static final String ROLE_CLAIMS = "role";
  
  /**
   * 生成Token
   */
  public static String createToken(String username,String role) {
    Map<String,Object> map = new HashMap<>();
    map.put(ROLE_CLAIMS, role);

    String token = Jwts
        .builder()
        .setSubject(username)
        .setClaims(map)
        .claim("username",username)
        .setIssuedAt(new Date())
        .setExpiration(new Date(System.currentTimeMillis() + EXPIRITION))
        .signWith(SignatureAlgorithm.HS256, APPSECRET_KEY).compact();
    return token;
  }

  /**
   * 校验Token
   */
  public static Claims checkJWT(String token) {
    try {
      final Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
      return claims;
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }

  /**
   * 从Token中获取username
   */
  public static String getUsername(String token){
    Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
    return claims.get("username").toString();
  }

  /**
   * 从Token中获取用户角色
   */
  public static String getUserRole(String token){
    Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
    return claims.get("role").toString();
  }

  /**
   * 校验Token是否过期
   */
  public static boolean isExpiration(String token){
    Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
    return claims.getExpiration().before(new Date());
  }
}

4、配置UserDetailsService的实现类 用于加载用户信息

import xxx.xxx.xxx.bean.Role; // 自己的包
import xxx.xxx.xxx.bean.User; // 自己的包
import xxx.xxx.xxx.mapper.UserMapper; // 自己的包
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

  @Autowired
  private UserMapper userMapper;

  @Override
  public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    if (s == null || "".equals(s))
    {
      throw new RuntimeException("用户不能为空");
    }
    // 调用方法查询用户
    User user = userMapper.findUserByUsername(s);
    if (user == null)
    {
      throw new RuntimeException("用户不存在");
    }
    List<SimpleGrantedAuthority> authorities = new ArrayList<>();
    for (Role role:userMapper.findRoleByUsername(s))
    {
      authorities.add(new SimpleGrantedAuthority("ROLE_"+role.getName()));
    }
    return new org.springframework.security.core.userdetails.User(user.getUsername(),"{noop}"+user.getPassword(),authorities);
  }
}

5、然后 配置两个拦截器

其中 一个用于登录 另一个用于鉴权

JWTAuthenticationFilter登录拦截器:

该拦截器用于获取用户登录的信息
至于具体的验证 只需创建一个token并调用authenticationManager的authenticate()方法
让Spring security验证即可 验证的事交给框架

import com.alibaba.fastjson.JSON;
import xxx.xxx.xxx.utils.JwtTokenUtil; // 自己的包
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;

/**
 * 验证用户名密码正确后 生成一个token并将token返回给客户端
 */
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

  private AuthenticationManager authenticationManager;

  public JWTAuthenticationFilter(AuthenticationManager authenticationManager)
  {
    this.authenticationManager = authenticationManager;
  }

  /**
   * 验证操作 接收并解析用户凭证
   */
  @Override
  public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {
    // 从输入流中获取到登录的信息
    // 创建一个token并调用authenticationManager.authenticate() 让Spring security进行验证
    return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(request.getParameter("username"),request.getParameter("password")));
  }

  /**
   * 验证【成功】后调用的方法
   * 若验证成功 生成token并返回
   */
  @Override
  protected void successfulAuthentication(HttpServletRequest request,HttpServletResponse response,FilterChain chain,Authentication authResult) throws IOException {
    User user= (User) authResult.getPrincipal();

    // 从User中获取权限信息
    Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
    // 创建Token
    String token = JwtTokenUtil.createToken(user.getUsername(), authorities.toString());

    // 设置编码 防止乱码问题
    response.setCharacterEncoding("UTF-8");
    response.setContentType("application/json; charset=utf-8");
    // 在请求头里返回创建成功的token
    // 设置请求头为带有"Bearer "前缀的token字符串
    response.setHeader("token", JwtTokenUtil.TOKEN_PREFIX + token);

    // 处理编码方式 防止中文乱码
    response.setContentType("text/json;charset=utf-8");
    // 将反馈塞到HttpServletResponse中返回给前台
    response.getWriter().write(JSON.toJSONString("登录成功"));
  }

  /**
   * 验证【失败】调用的方法
   */
  @Override
  protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
    String returnData="";
    // 账号过期
    if (failed instanceof AccountExpiredException) {
      returnData="账号过期";
    }
    // 密码错误
    else if (failed instanceof BadCredentialsException) {
      returnData="密码错误";
    }
    // 密码过期
    else if (failed instanceof CredentialsExpiredException) {
      returnData="密码过期";
    }
    // 账号不可用
    else if (failed instanceof DisabledException) {
      returnData="账号不可用";
    }
    //账号锁定
    else if (failed instanceof LockedException) {
      returnData="账号锁定";
    }
    // 用户不存在
    else if (failed instanceof InternalAuthenticationServiceException) {
      returnData="用户不存在";
    }
    // 其他错误
    else{
      returnData="未知异常";
    }

    // 处理编码方式 防止中文乱码
    response.setContentType("text/json;charset=utf-8");
    // 将反馈塞到HttpServletResponse中返回给前台
    response.getWriter().write(JSON.toJSONString(returnData));
  }
}

JWTAuthorizationFilter权限校验拦截器:

当访问需要权限校验的URL(当然 该URL也是需要经过配置的) 则会来到此拦截器 在该拦截器中对传来的Token进行校验
只需告诉Spring security该用户是否已登录 并且是什么角色 拥有什么权限即可

import xxx.xxx.xxx.utils.JwtTokenUtil; // 自己的包
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;

/**
 * 登录成功后 走此类进行鉴权操作
 */
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {

  public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
    super(authenticationManager);
  }

  /**
   * 在过滤之前和之后执行的事件
   */
  @Override
  protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain) throws IOException, ServletException {
    String tokenHeader = request.getHeader(JwtTokenUtil.TOKEN_HEADER);

    // 若请求头中没有Authorization信息 或是Authorization不以Bearer开头 则直接放行
    if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtil.TOKEN_PREFIX))
    {
      chain.doFilter(request, response);
      return;
    }

    // 若请求头中有token 则调用下面的方法进行解析 并设置认证信息
    SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
    super.doFilterInternal(request, response, chain);
  }

  /**
   * 从token中获取用户信息并新建一个token
   *
   * @param tokenHeader 字符串形式的Token请求头
   * @return 带用户名和密码以及权限的Authentication
   */
  private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
    // 去掉前缀 获取Token字符串
    String token = tokenHeader.replace(JwtTokenUtil.TOKEN_PREFIX, "");
    // 从Token中解密获取用户名
    String username = JwtTokenUtil.getUsername(token);
    // 从Token中解密获取用户角色
    String role = JwtTokenUtil.getUserRole(token);
    // 将[ROLE_XXX,ROLE_YYY]格式的角色字符串转换为数组
    String[] roles = StringUtils.strip(role, "[]").split(", ");
    Collection<SimpleGrantedAuthority> authorities=new ArrayList<>();
    for (String s:roles)
    {
      authorities.add(new SimpleGrantedAuthority(s));
    }
    if (username != null)
    {
      return new UsernamePasswordAuthenticationToken(username, null,authorities);
    }
    return null;
  }
}

6、再配置一个自定义类 用于进行匿名用户访问资源时无权限的处理

该类需实现AuthenticationEntryPoint

import com.alibaba.fastjson.JSONObject;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {

  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response,
             AuthenticationException authException) throws IOException, ServletException {
    response.setCharacterEncoding("utf-8");
    response.setContentType("text/javascript;charset=utf-8");
    response.getWriter().print(JSONObject.toJSONString("您未登录,没有访问权限"));
  }
}

7、最后 将这些组件组装到一起即可

创建一个自定义的配置类 继承WebSecurityConfigurerAdapter
在该类上 需加@EnableWebSecurity注解 配置Web安全过滤器和启用全局认证机制

import xxx.xxx.xxx.JWTAuthenticationEntryPoint; // 自己的包
import xxx.xxx.xxx.xxx.JWTAuthenticationFilter; // 自己的包
import xxx.xxx.xxx.xxx.JWTAuthorizationFilter; // 自己的包
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  @Qualifier("userDetailsServiceImpl")
  private UserDetailsService userDetailsService;

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService);
  }

  /**
   * 安全配置
   */
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    // 跨域共享
    http.cors()
        .and()
        // 跨域伪造请求限制无效
        .csrf().disable()
        .authorizeRequests()
        // 访问/data需要ADMIN角色
        .antMatchers("/data").hasRole("ADMIN")
        // 其余资源任何人都可访问
        .anyRequest().permitAll()
        .and()
        // 添加JWT登录拦截器
        .addFilter(new JWTAuthenticationFilter(authenticationManager()))
        // 添加JWT鉴权拦截器
        .addFilter(new JWTAuthorizationFilter(authenticationManager()))
        .sessionManagement()
        // 设置Session的创建策略为:Spring Security永不创建HttpSession 不使用HttpSession来获取SecurityContext
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        // 异常处理
        .exceptionHandling()
        // 匿名用户访问无权限资源时的异常
        .authenticationEntryPoint(new JWTAuthenticationEntryPoint());
  }

  /**
   * 跨域配置
   * @return 基于URL的跨域配置信息
   */
  @Bean
  CorsConfigurationSource corsConfigurationSource() {
    final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    // 注册跨域配置
    source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
    return source;
  }
}

定义一个用于测试的对外映射接口:

@RestController
public class UserController {

  @GetMapping("/data")
  private ResponseUtil data()
  {
    return "This is data.";
  }
}

默认登录路径是/login 用POST请求发送

若要修改默认的登录路径 只需要在自己定义的登录过滤器JWTAuthenticationFilter的构造方法里进行配置即可
比如 若想修改为/api/login:

public JWTAuthenticationFilter(AuthenticationManager authenticationManager)
{
   this.authenticationManager = authenticationManager;
   // 设置登录URL
   super.setFilterProcessesUrl("/api/login");
}

登录时 参数的属性名分别是username和password 不能改动:

登录成功后会返回一个Token:

在请求需要权限的接口路径时 若不带上Token 则会提示没有访问权限

带上Token后再次请求 即可正常访问:

注:Token的前面要带有Bearer 的前缀


这样 一个基本的实现就差不多完成了

为简单演示 在该案例中就不对密码进行加密了 实际开发是需要对明文密码加密后存储的 推荐用BCrypt进行加密和解密
为节省篇幅 用于注册的接口也不写了 实际上在注册接口传入的密码也需要用BCrypt加密后再存入数据库中
还可以用Redis进行Token的存储 这些都是后话了

到此这篇关于SpringBoot集成Spring Security用JWT令牌实现登录和鉴权的方法的文章就介绍到这了,更多相关SpringBoot JWT令牌登录和鉴权内容请搜索易盾网络以前的文章或继续浏览下面的相关文章希望大家以后多多支持易盾网络!

网友评论