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

 更新时间:2020年5月18日 08:11  点击:2143

最近在做项目的过程中 需要用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令牌登录和鉴权内容请搜索猪先飞以前的文章或继续浏览下面的相关文章希望大家以后多多支持猪先飞!

[!--infotagslink--]

相关文章

  • 解决springboot使用logback日志出现LOG_PATH_IS_UNDEFINED文件夹的问题

    这篇文章主要介绍了解决springboot使用logback日志出现LOG_PATH_IS_UNDEFINED文件夹的问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-04-28
  • php中登录后跳转回原来要访问的页面实例

    在很多网站用户先访问一个要登录的页面,但当时没有登录后来登录了,等待用户登录成功之后肯定希望返回到上次访问的页面,下面我就来给大家介绍登录后跳转回原来要访问的页...2016-11-25
  • SpringBoot实现excel文件生成和下载

    这篇文章主要为大家详细介绍了SpringBoot实现excel文件生成和下载,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-02-09
  • php中用curl模拟登录discuz以及模拟发帖

    本文章完美的利用了php的curl功能实现模拟登录discuz以及模拟发帖,本教程供参考学习哦。 代码如下 复制代码 <?php $discuz_url = &lsquo;ht...2016-11-25
  • 详解springBoot启动时找不到或无法加载主类解决办法

    这篇文章主要介绍了详解springBoot启动时找不到或无法加载主类解决办法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-09-16
  • SpringBoot集成Redis实现消息队列的方法

    这篇文章主要介绍了SpringBoot集成Redis实现消息队列的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-02-10
  • Ruby on Rails实现最基本的用户注册和登录功能的教程

    这里我们主要以has_secure_password的用户密码验证功能为中心,来讲解Ruby on Rails实现最基本的用户注册和登录功能的教程,需要的朋友可以参考下...2020-06-30
  • 解决Springboot get请求是参数过长的情况

    这篇文章主要介绍了解决Springboot get请求是参数过长的情况,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-09-17
  • PHP中SSO Cookie登录分析和实现

    什么是SSO?单点登录SSO(Single Sign-On)是身份管理中的一部分。SSO的一种较为通俗的定义是:SSO是指访问同一服务器不同应用中的受保护资源的同一用户,只需要登录一次,即通过一个应用中的安全验证后,再访问其他应用中的受保护...2015-11-08
  • Spring Boot项目@RestController使用重定向redirect方式

    这篇文章主要介绍了Spring Boot项目@RestController使用重定向redirect方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-09-02
  • Springboot+TCP监听服务器搭建过程图解

    这篇文章主要介绍了Springboot+TCP监听服务器搭建过程,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2020-10-28
  • springBoot 项目排除数据库启动方式

    这篇文章主要介绍了springBoot 项目排除数据库启动方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-09-10
  • PHP中SSO Cookie登录分析和实现

    什么是SSO?单点登录SSO(Single Sign-On)是身份管理中的一部分。SSO的一种较为通俗的定义是:SSO是指访问同一服务器不同应用中的受保护资源的同一用户,只需要登录一次,即通过一个应用中的安全验证后,再访问其他应用中的受保护...2015-11-08
  • php有效防止同一用户多次登录

    【问题描述】:同一用户在同一时间多次登录如果不能检测出来,是危险的。因为,你无法知道是否有其他用户在登录你的账户。如何禁止同一用户多次登录呢? 【解决方案】 (1) 每次登录,身份认证成功后,重新产生一个session_id。 s...2015-11-24
  • vue实现用户登录切换

    这篇文章主要为大家详细介绍了vue实现用户登录切换,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-04-22
  • 详解SpringBoot之访问静态资源(webapp...)

    这篇文章主要介绍了详解SpringBoot之访问静态资源(webapp...),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-09-14
  • SpringBoot接口接收json参数解析

    这篇文章主要介绍了SpringBoot接口接收json参数解析,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-10-19
  • springboot中使用@Transactional注解事物不生效的坑

    这篇文章主要介绍了springboot中使用@Transactional注解事物不生效的原因,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-01-26
  • 修改mysql密码phpmyadmin不能登录

    出现phpmyadmin不能登录是我在修改我mysql服务器密码之后导致的,后来百度了相关的原因,原来是修改了mysql密码之后我们还需要在phpmyadmin目录中去修改config.inc.php中...2016-11-25
  • vue element后台鉴权流程分析

    这篇文章主要介绍了vue element后台鉴权流程分析,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-04-10