Spring Security权限控制 + JWT Token 认证

文章目录

  • 一、 前言
    • 1. spring-security中核心概念
    • 2. Spring Security的核心拦截器
    • 3. JWT认证
  • 二、关键代码讲解
    • 1. 登陆阶段流程
    • 2. Token验证和权限控制阶段流程
  • 三、 关键代码
    • 一、介绍
    • 二、基础Security框架搭建
    • 三、Spring Security的权限控制关键类。
    • 四、JWT关键类
    • 五. 异常信息处理类
    • 六. 配置类
    • 七、演示
  • 四、 其它

一、 前言

项目实现了Spring Security 权限控制 + Jwt Token认证。

1. spring-security中核心概念

类名 概念
AuthenticationManager 用户认证的管理类,所有的认证请求(比如login)都会通过提交一个token给AuthenticationManager的authenticate()方法来实现。当然事情肯定不是它来做,具体校验动作会由AuthenticationManager将请求转发给具体的实现类来做。根据实现反馈的结果再调用具体的Handler来给用户以反馈。
AuthenticationProvider 认证的具体实现类,一个provider是一种认证方式的实现,比如提交的用户名密码我是通过和DB中查出的user记录做比对实现的,那就有一个DaoProvider;如果我是通过CAS请求单点登录系统实现,那就有一个CASProvider。按照Spring一贯的作风,主流的认证方式它都已经提供了默认实现,比如DAO、LDAP、CAS、OAuth2等。前面讲了AuthenticationManager只是一个代理接口,真正的认证就是由AuthenticationProvider来做的。一个AuthenticationManager可以包含多个Provider,每个provider通过实现一个support方法来表示自己支持那种Token的认证。AuthenticationManager默认的实现类是ProviderManager。
UserDetailService 用户认证通过Provider来做,所以Provider需要拿到系统已经保存的认证信息,获取用户信息的接口spring-security抽象成UserDetailService。
AuthenticationToken 所有提交给AuthenticationManager的认证请求都会被封装成一个Token的实现,比如最容易理解的UsernamePasswordAuthenticationToken。
SecurityContext 当用户通过认证之后,就会为这个用户生成一个唯一的SecurityContext,里面包含用户的认证信息Authentication。通过SecurityContext我们可以获取到用户的标识Principle和授权信息GrantedAuthrity。在系统的任何地方只要通过SecurityHolder.getSecruityContext()就可以获取到SecurityContext。

2. Spring Security的核心拦截器

拦截器 释义
HttpSessionContextIntegrationFilter 位于过滤器顶端,第一个起作用的过滤器。用途一,在执行其他过滤器之前,率先判断用户的session中是否已经存在一个SecurityContext了。如果存在,就把SecurityContext拿出来,放到SecurityContextHolder中,供Spring Security的其他部分使用。如果不存在,就创建一个SecurityContext出来,还是放到SecurityContextHolder中,供Spring Security的其他部分使用。用途二,在所有过滤器执行完毕后,清空SecurityContextHolder,因为SecurityContextHolder是基于ThreadLocal的,如果在操作完成后清空ThreadLocal,会受到服务器的线程池机制的影响。
LogoutFilter 只处理注销请求,默认为/j_spring_security_logout。用途是在用户发送注销请求时,销毁用户session,清空SecurityContextHolder,然后重定向到注销成功页面。可以与rememberMe之类的机制结合,在注销的同时清空用户cookie。
AuthenticationProcessingFilter 处理form登陆的过滤器,与form登陆有关的所有操作都是在此进行的。默认情况下只处理/j_spring_security_check请求,这个请求应该是用户使用form登陆后的提交地址此过滤器执行的基本操作时,通过用户名和密码判断用户是否有效,如果登录成功就跳转到成功页面(可能是登陆之前访问的受保护页面,也可能是默认的成功页面),如果登录失败,就跳转到失败页面。
DefaultLoginPageGeneratingFilter 此过滤器用来生成一个默认的登录页面,默认的访问地址为/spring_security_login,这个默认的登录页面虽然支持用户输入用户名,密码,也支持rememberMe功能,但是因为太难看了,只能是在演示时做个样子,不可能直接用在实际项目中。
BasicProcessingFilter 此过滤器用于进行basic验证,功能与AuthenticationProcessingFilter类似,只是验证的方式不同。
SecurityContextHolderAwareRequestFilter 此过滤器用来包装客户的请求。目的是在原始请求的基础上,为后续程序提供一些额外的数据。比如getRemoteUser()时直接返回当前登陆的用户名之类的。
RememberMeProcessingFilter 此过滤器实现RememberMe功能,当用户cookie中存在rememberMe的标记,此过滤器会根据标记自动实现用户登陆,并创建SecurityContext,授予对应的权限。
AnonymousProcessingFilter 为了保证操作统一性,当用户没有登陆时,默认为用户分配匿名用户的权限。
ExceptionTranslationFilter 此过滤器的作用是处理中FilterSecurityInterceptor抛出的异常,然后将请求重定向到对应页面,或返回对应的响应错误代码
SessionFixationProtectionFilter 防御会话伪造攻击。有关防御会话伪造的详细信息
FilterSecurityInterceptor 用户的权限控制都包含在这个过滤器中。功能一:如果用户尚未登陆,则抛出AuthenticationCredentialsNotFoundException“尚未认证异常”。功能二:如果用户已登录,但是没有访问当前资源的权限,则抛出AccessDeniedException“拒绝访问异常”。功能三:如果用户已登录,也具有访问当前资源的权限,则放行。我们可以通过配置方式来自定义拦截规则

所有的过滤器都会实现SpringSecurityFilter安全过滤器

Spring Security权限控制 + JWT Token 认证_第1张图片

3. JWT认证

我理解的,就是用户凭证不再由服务端保存,而是由客户端自己保存。即客户端登陆后,将加密登陆凭证交于客户端,客户端并不明白凭证有何意义,只知道登陆需要使用。在登陆访问时我们获取到登陆凭证进行解密,获取到当前用户信息。同时用户凭证隔一段时间会失效。具体介绍可以
https://www.jianshu.com/p/12b609e40029 的介绍

二、关键代码讲解

文末会给出项目地址,所以基础搭建不在赘述。

1. 登陆阶段流程

1. 登陆阶段流程图。
中间省略了Spring Security 的某些调用。仅用来描绘自己代码的逻辑。
Spring Security权限控制 + JWT Token 认证_第2张图片
2. 登陆阶段讲解。(请自觉忽略我的背景图。。。。。)

1.我们在配置类中添加了两个自定义拦截器 JwtLoginFilterJwtTokenFilter 。我们这里关注 JwtLoginFilter
Spring Security权限控制 + JWT Token 认证_第3张图片


2.JwtLoginFilter 继承了 UsernamePasswordAuthenticationFilterUsernamePasswordAuthenticationFilter 是用来处理身份验证的表单提交。也就是说 我们在 JwtLoginFilter 中处理表单提交的身份信息。Spring Security权限控制 + JWT Token 认证_第4张图片

我们进去 UsernamePasswordAuthenticationFilter 可以看到在其构造函数指定了拦截路径,即默认拦截 Post 请求方式的 /login 请求。我们可以在配置类中通过 formLogin().loginProcessingUrl(“XXXX”) 来指定登陆路径。
Spring Security权限控制 + JWT Token 认证_第5张图片
3. 在 JwtLoginFilter 中,我们获取参数username,password 来获取提交的用户名和密码,封装了凭证后进行登陆信息的校验。this.getAuthenticationManager() 来获取用户认证的管理类 。用户认证的管理类,所有的认证请求(比如login)都会通过提交一个token给AuthenticationManagerauthenticate()方法来实现。当然事情肯定不是它来做,具体校验动作会由AuthenticationManager将请求转发给具体的实现类来做。我们这里的实现类即 JwtAuthenticationProvider
Spring Security权限控制 + JWT Token 认证_第6张图片
4. 跳转到 JwtAuthenticationProvider.authenticate 中进行逻辑处理(因为在配置类中指定了通过 authenticationProvider方法配置了校验类)。 JwtAuthenticationProvider.authenticate 的具体校验,根据注释就可以清楚了。
Spring Security权限控制 + JWT Token 认证_第7张图片
5. 校验成功则会调用 JwtLoginSuccessHandler.生成一个token并返回给用户
Spring Security权限控制 + JWT Token 认证_第8张图片

5.失败则会调用 JwtLoginFailureHandler 返回错误信息
Spring Security权限控制 + JWT Token 认证_第9张图片
6. 至于为什么会调用这两个类,是因为我们在配置类中进行了初始化配置:
Spring Security权限控制 + JWT Token 认证_第10张图片
并且在拦截链路中加入了这两个拦截器。
对于添加拦截器以下三个方法:

  • addFilterBefore(Filter filter, Class beforeFilter) 在 beforeFilter 之前添加 filter
  • addFilterAfter(Filter filter, Class afterFilter) 在 afterFilter 之后添加 filter
  • addFilterAt(Filter filter, Class atFilter) 在 atFilter 相同位置添加 filter, 此 filter 不覆盖 filter

Spring Security权限控制 + JWT Token 认证_第11张图片

2. Token验证和权限控制阶段流程

1. Token验证流程图
Spring Security权限控制 + JWT Token 认证_第12张图片
2. 代码讲解
1.由于我们在拦截链中加入了JwtLoginFilterJwtTokenFilter 。而 JwtLoginFilter 上面说过只拦截登陆路径。其余路径则会被 JwtTokenFilter 拦截。
Spring Security权限控制 + JWT Token 认证_第13张图片
2. JwtTokenFilter具体代码如下。
Spring Security权限控制 + JWT Token 认证_第14张图片

  1. 如果验证通过,则将token保存在Security上下文中。并进行下一步调用。
    Spring Security权限控制 + JWT Token 认证_第15张图片
  2. 用户的请求会到达 JwtFilterInvocationSecurityMetadataSource 中。 JwtFilterInvocationSecurityMetadataSource 根据当前路径获取到有资格访问当前页面的角色列表(比如 Admin,Teacher 等)。
    Spring Security权限控制 + JWT Token 认证_第16张图片
  3. 随后调用链路到了 JwtUrlAccessDecisionManager 中,在这里来校验当前用户是否具备所需要的角色。校验通过,则允许访问,否则抛出 AccessDeniedException 异常。
    Spring Security权限控制 + JWT Token 认证_第17张图片
    Spring Security权限控制 + JWT Token 认证_第18张图片
  4. JwtAccessDeniedHandler 返回错误信息,这里没有返回403错误。而是算他访问成功,提示权限不足。具体根据业务调整
    Spring Security权限控制 + JWT Token 认证_第19张图片

三、 关键代码

一、介绍

  1. 数据库的设置,本项目中其实只是使用了elst_menu,else_role,elst_user 以及其关联表,elst_user_role, elst_role_menu。
  2. 一个User具有多个Role,一个Role对应可以访问多个Menu。
    Spring Security权限控制 + JWT Token 认证_第20张图片

二、基础Security框架搭建

JwtUserDetails
其中: 声明一个Spring Security 的User实例,供Spring Security 使用。
roles 是当前用户具备的角色列表

package com.securityjwtdemo.entity.security;

import com.securityjwtdemo.entity.ElstRole;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @Data: 2019/10/30
 * @Des:
 */
public class JwtUserDetails implements UserDetails {
    private Integer id;

    private String userId;

    private String userName;

    private String userPwd;

    private Short userEnabled;

    private List<ElstRole> roles;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getUserPwd() {
        return userPwd;
    }

    public void setUserPwd(String userPwd) {
        this.userPwd = userPwd;
    }

    public Short getUserEnabled() {
        return userEnabled;
    }

    public void setUserEnabled(Short userEnabled) {
        this.userEnabled = userEnabled;
    }

    public List<ElstRole> getRoles() {
        return roles;
    }

    public void setRoles(List<ElstRole> roles) {
        this.roles = roles;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles == null ? new ArrayList<SimpleGrantedAuthority>() : roles.stream().map(r ->new SimpleGrantedAuthority(r.getRoleId())).collect(Collectors.toList());
    }

    @Override

    public String getPassword() {
        return this.userPwd;
    }

    @Override
    public String getUsername() {
        return this.userName;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return 1 == userEnabled;
    }
}

JwtUserDetailsService :
根据 username 获取当前用户。再次强调,这里的username并不是数据库中user_name,而是能唯一确定用户的字段,这里实际意思是 user_id。在进行用户密码等校验时,会调用 UserDetailsService.loadUserByUsername 方法获取到登陆用户,再进行校验。

package com.securityjwtdemo.service.jwtsecurity;

import com.securityjwtdemo.dao.ElstUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
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.Component;

/**
 * @Data: 2019/10/30
 * @Des:
 */
@Component
public class JwtUserDetailsService implements UserDetailsService {
    @Autowired
    private ElstUserMapper elstUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return elstUserMapper.loadUserByUsername(username);
    }
}

三、Spring Security的权限控制关键类。

JwtFilterInvocationSecurityMetadataSource

package com.securityjwtdemo.common.config.security;

import com.securityjwtdemo.dao.ElstMenuMapper;
import com.securityjwtdemo.entity.info.ElstMenuInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

import java.util.Collection;
import java.util.List;

/**
 * @Data: 2019/10/31
 * @Des: 获取有权访问当前url的角色列表
 */
@Component
public class JwtFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    private ElstMenuMapper elstMenuMapper;

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        List<ElstMenuInfo> allMenu = elstMenuMapper.getAllMenuInfo();
        for (ElstMenuInfo menu : allMenu) {
            if (antPathMatcher.match(menu.getMenuUrl(), requestUrl) && menu.getRoles().size() > 0) {
                String[] roleIds = menu.getRoles().stream().map(r -> r.getRoleId()).toArray(String[]::new);
                return SecurityConfig.createList(roleIds);
            }
        }
        // 如果没有匹配,则默认全部可以访问
        return SecurityConfig.createList();
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

JwtUrlAccessDecisionManager : 这个类用来校验当前用户是否具备访问当前路径的角色

package com.securityjwtdemo.common.config.security;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.Collection;
import java.util.Iterator;

/**
 * @Data: 2019/10/31
 * @Des: 校验当前用户是否具有访问该路径的角色
 */
@Component
public class JwtUrlAccessDecisionManager implements AccessDecisionManager {
    /**
     *
     * @param authentication  当前用户凭证 -- > JwtTokenFilter中将通过验证的用户保存在Security上下文中, 即传入了这里
     * @param object  当前请求路径
     * @param configAttributes  当前请求路径所需要的角色列表 -- > 从 JwtFilterInvocationSecurityMetadataSource 返回
     * @throws AccessDeniedException
     * @throws InsufficientAuthenticationException
     */
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        Iterator<ConfigAttribute> iterator = configAttributes.iterator();
        while (iterator.hasNext()) {
            ConfigAttribute ca = iterator.next();
            //当前请求需要的权限
            String needRole = ca.getAttribute();
            if (StringUtils.isEmpty(needRole)) {
                return;
            }
            //当前用户所具有的权限
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("权限不足!");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return false;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return false;
    }
}

ElstUserMapper.xml
编写了获取用户角色的逻辑

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.securityjwtdemo.dao.ElstUserMapper">
    <resultMap id="BaseResultMap" type="com.securityjwtdemo.entity.ElstUser">
        <id column="id" jdbcType="INTEGER" property="id"/>
        <result column="user_id" jdbcType="VARCHAR" property="userId"/>
        <result column="user_name" jdbcType="VARCHAR" property="userName"/>
        <result column="user_pwd" jdbcType="VARCHAR" property="userPwd"/>
        <result column="user_enabled" jdbcType="SMALLINT" property="userEnabled"/>
    </resultMap>

    <sql id="Base_Column_List">
    id, user_id, user_name, user_pwd, user_enabled
  </sql>

    <resultMap id="loadUserByUsernameResultMap" type="com.securityjwtdemo.entity.security.JwtUserDetails">
        <result column="user_id" property="userId"></result>
        <collection property="roles" select="com.securityjwtdemo.dao.ElstRoleMapper.selectRoleByUser" column="user_id"
                    ofType="com.securityjwtdemo.entity.ElstRole"/>
    </resultMap>

    <select id="loadUserByUsername" resultMap="loadUserByUsernameResultMap">
        select
        <include refid="Base_Column_List"/>
        from elst_user
        where user_id = #{userId}
    </select>
</mapper>

ElstMenuMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.securityjwtdemo.dao.ElstMenuMapper">
  <resultMap id="BaseResultMap" type="com.securityjwtdemo.entity.ElstMenu">
    <id column="id" jdbcType="INTEGER" property="id" />
    <result column="menu_id" jdbcType="VARCHAR" property="menuId" />
    <result column="menu_name" jdbcType="VARCHAR" property="menuName" />
    <result column="parent_id" jdbcType="VARCHAR" property="parentId" />
    <result column="menu_url" jdbcType="VARCHAR" property="menuUrl" />
    <result column="menu_path" jdbcType="VARCHAR" property="menuPath" />
    <result column="menu_enabled" jdbcType="SMALLINT" property="menuEnabled" />
  </resultMap>

  <sql id="Base_Column_List">
    id, menu_id, menu_name, parent_id, menu_url, menu_path, menu_enabled
  </sql>

  <resultMap id="Menu_Role_Info" type="com.securityjwtdemo.entity.info.ElstMenuInfo">
    <collection property="roles" select="com.securityjwtdemo.dao.ElstRoleMapper.selectRoleByMenu" column="menu_id"
                ofType="com.securityjwtdemo.entity.ElstRole" ></collection>
  </resultMap>

  <select id="getAllMenuInfo" resultMap="Menu_Role_Info">
    select
    <include refid="Base_Column_List"></include>
    from elst_menu
  </select>
</mapper>

ElstRoleMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.securityjwtdemo.dao.ElstRoleMapper">
  <resultMap id="BaseResultMap" type="com.securityjwtdemo.entity.ElstRole">
    <id column="id" jdbcType="INTEGER" property="id" />
    <result column="role_id" jdbcType="VARCHAR" property="roleId" />
    <result column="role_name" jdbcType="VARCHAR" property="roleName" />
    <result column="zh_name" jdbcType="VARCHAR" property="zhName" />
  </resultMap>

  <sql id="Base_Column_List">
    id, role_id, role_name, zh_name
  </sql>

  <select id="selectRoleByUser" resultType="com.securityjwtdemo.entity.ElstRole">
      select
        r.id, r.role_id, r.role_name, r.zh_name
      from elst_role r
      LEFT JOIN elst_user_role ur ON ur.role_id = r.role_id
      WHERE ur.user_id = #{userId}
  </select>

    <select id="selectRoleByMenu" resultType="com.securityjwtdemo.entity.ElstRole">
      select
        r.id, r.role_id, r.role_name, r.zh_name
      from elst_role r
      LEFT JOIN elst_role_menu rm ON rm.role_id = r.role_id
      WHERE rm.menu_id = #{menuId}
  </select>
</mapper>

四、JWT关键类

JwtLoginToken :
自定义 Token 类,保存当前用户的信息

package com.securityjwtdemo.entity.security;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

/**
 * @Data: 2019/10/30
 * @Des: 用户鉴权 : 保存当前用户的认证信息,如认证状态,用户名密码,拥有的权限等
 */
public class JwtLoginToken extends AbstractAuthenticationToken {

    /**登录用户信息*/
    private final Object principal;
    /**密码*/
    private Object credentials;

    /**创建一个未认证的授权令牌,
     * 这时传入的principal是用户名
     *
     */
    public JwtLoginToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }

    /**创建一个已认证的授权令牌,如注释中说的那样,这个方法应该由AuthenticationProvider来调用
     * 也就是我们写的JwtAuthenticationProvider,有它完成认证后再调用这个方法,
     * 这时传入的principal为从userService中查出的UserDetails
     * @param principal
     * @param credentials
     * @param authorities
     */
    public JwtLoginToken(Object principal, Object credentials,
                         Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return this.credentials;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }
}

JwtLoginFilter
登陆拦截器,用户进行登录信息校验

package com.securityjwtdemo.filter.security;

import com.securityjwtdemo.entity.security.JwtLoginToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.WebAuthenticationDetails;

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

/**
 * @Data: 2019/10/30
 * @Des: 用户登录验证拦截器 --  执行顺序在UsernamePasswordAuthenticationFilter 拦截器后
 */

public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {

    /**
     * 拦截逻辑
     *
     * @param request
     * @param response
     * @return
     * @throws AuthenticationException
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String userName = request.getParameter("username");
        String password = request.getParameter("password");
        //创建未认证的凭证(etAuthenticated(false)),注意此时凭证中的主体principal为用户名
        JwtLoginToken jwtLoginToken = new JwtLoginToken(userName, password);
        //将认证详情(ip,sessionId)写到凭证
        jwtLoginToken.setDetails(new WebAuthenticationDetails(request));
        //AuthenticationManager获取受支持的AuthenticationProvider(这里也就是JwtAuthenticationProvider),
        //生成已认证的凭证,此时凭证中的主体为userDetails  --- 这里会委托给AuthenticationProvider实现类来验证
        // 即 跳转到 JwtAuthenticationProvider.authenticate 方法中认证
        Authentication authenticatedToken = this.getAuthenticationManager().authenticate(jwtLoginToken);
        return authenticatedToken;

    }
}

JwtAuthenticationProvider :
自定义的用户逻辑校验,在这里进行了用户的信息校验。

package com.securityjwtdemo.common.config.security;

import com.securityjwtdemo.entity.security.JwtLoginToken;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @Data: 2019/10/30
 * @Des: 用户角色校验具体实现
 */
public class JwtAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    private PasswordEncoder passwordEncoder;

    public JwtAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.userDetailsService = userDetailsService;
        this.passwordEncoder = passwordEncoder;
    }

    /**
     * 鉴权具体逻辑
     *
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        UserDetails userDetails = userDetailsService.loadUserByUsername(authentication.getName());
        //转换authentication 认证时会先调用support方法,受支持才会调用,所以能强转
        JwtLoginToken jwtLoginToken = (JwtLoginToken) authentication;
        defaultCheck(userDetails);
        // 用户名密码校验 具体逻辑
        additionalAuthenticationChecks(userDetails, jwtLoginToken);
        //构造已认证的authentication
        JwtLoginToken authenticatedToken = new JwtLoginToken(userDetails, jwtLoginToken.getCredentials(), userDetails.getAuthorities());
        authenticatedToken.setDetails(jwtLoginToken.getDetails());
        return authenticatedToken;
    }

    /**
     * 是否支持当前类
     *
     * @param authentication
     * @return
     */
    public boolean supports(Class<?> authentication) {
        return (JwtLoginToken.class
                .isAssignableFrom(authentication));
    }

    /**
     * 一些默认信息的检查
     *
     * @param user
     */
    private void defaultCheck(UserDetails user) {
        if (!user.isAccountNonLocked()) {
            throw new LockedException("User account is locked");
        }

        if (!user.isEnabled()) {
            throw new DisabledException("User is disabled");
        }

        if (!user.isAccountNonExpired()) {
            throw new AccountExpiredException("User account has expired");
        }
    }

    /**
     * 检查密码是否正确
     *
     * @param userDetails
     * @param authentication
     * @throws AuthenticationException
     */
    private void additionalAuthenticationChecks(UserDetails userDetails, JwtLoginToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            throw new BadCredentialsException("Bad credentials");
        }
        String presentedPassword = authentication.getCredentials().toString();
        if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            throw new BadCredentialsException("Bad credentials");
        }
    }

}

JwtTokenFilter :
token有效性校验的拦截器

package com.securityjwtdemo.filter.security;

import com.alibaba.fastjson.JSON;
import com.securityjwtdemo.common.JsonResponseStatus;
import com.securityjwtdemo.common.JsonResult;
import com.securityjwtdemo.entity.security.JwtLoginToken;
import com.securityjwtdemo.entity.security.JwtUserDetails;
import com.securityjwtdemo.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

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

/**
 * @Data: 2019/10/30
 * @Des: Token有效性验证拦截器
 */
public class JwtTokenFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        try {
            String token = httpServletRequest.getHeader("Authentication");
            if (StringUtils.isEmpty(token)) {
                httpServletResponse.setContentType("application/json;charset=UTF-8");
                JsonResult<String> jsonResult = new JsonResult<>();
                jsonResult.setFail(JsonResponseStatus.TokenFail.getCode(), "未登录");
                httpServletResponse.getWriter().write(JSON.toJSONString(jsonResult));
                return;
            }

            Claims claims = JwtUtils.parseJWT(token);
            if (JwtUtils.isTokenExpired(claims)) {
                httpServletResponse.setContentType("application/json;charset=UTF-8");
                JsonResult<String> jsonResult = new JsonResult<>();
                jsonResult.setFail(JsonResponseStatus.TokenFail.getCode(), "登陆失效,请重新登陆");
                httpServletResponse.getWriter().write(JSON.toJSONString(jsonResult));
                return;
            }

            JwtUserDetails user = JSON.parseObject(claims.get("userDetails", String.class), JwtUserDetails.class);
            JwtLoginToken jwtLoginToken = new JwtLoginToken(user, "", user.getAuthorities());
            jwtLoginToken.setDetails(new WebAuthenticationDetails(httpServletRequest));
            SecurityContextHolder.getContext().setAuthentication(jwtLoginToken);
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        } catch (Exception e) {
            throw new BadCredentialsException("登陆凭证失效,请重新登陆");
        }

    }

}

JwtLoginSuccessHandler
登陆验证成功后进入这里,生成token并返回。

package com.securityjwtdemo.service.jwtsecurity;

import com.alibaba.fastjson.JSON;
import com.securityjwtdemo.common.Constants;
import com.securityjwtdemo.common.JsonResult;
import com.securityjwtdemo.entity.security.JwtUserDetails;
import com.securityjwtdemo.utils.JwtUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

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

/**
 * @Data: 2019/10/30
 * @Des: 登陆验证成功处理
 */
@Component
public class JwtLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        JwtUserDetails jwtUserDetails = (JwtUserDetails) authentication.getPrincipal();
        String json = JSON.toJSONString(jwtUserDetails);
        String jwtToken = JwtUtils.createJwtToken(json, Constants.DEFAULT_TOKEN_TIME_MS);
        //签发token
        JsonResult<String> jsonResult = new JsonResult<>();
        jsonResult.setSuccess(jwtToken);
        response.getWriter().write(JSON.toJSONString(jsonResult));
    }
}

JwtLoginFailureHandler :
登陆验证失败后会跳到这里

package com.securityjwtdemo.service.jwtsecurity;

import com.alibaba.fastjson.JSON;
import com.securityjwtdemo.common.JsonResponseStatus;
import com.securityjwtdemo.common.JsonResult;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

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

/**
 * @Data : 2019/10/31
 * @Des :  登陆验证失败处理
 */
@Component
public class JwtLoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        String msg = "登陆失败";
        if (exception instanceof BadCredentialsException ||
                exception instanceof UsernameNotFoundException) {
            msg = "账户名或者密码输入错误!";
        } else if (exception instanceof LockedException) {
            msg = "账户被锁定,请联系管理员!";
        } else if (exception instanceof CredentialsExpiredException) {
            msg = "密码过期,请联系管理员!";
        } else if (exception instanceof AccountExpiredException) {
            msg = "账户过期,请联系管理员!";
        } else if (exception instanceof DisabledException) {
            msg = "账户被禁用,请联系管理员!";
        }

        JsonResult<String> jsonResult = new JsonResult<>();
        jsonResult.setFail(JsonResponseStatus.LoginError.getCode(), msg);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(jsonResult));
    }
}

JwtUtils
jwt 工具类

package com.securityjwtdemo.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @author: GYB
 * createAt: 2018/9/14
 */
@Component
public class JwtUtils {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    public static final long DEFAULT_TOKEN_TIME_MS = 30 * 60 * 1000;

   /*
    iss: 该JWT的签发者
    sub: 该JWT所面向的用户
    aud: 接收该JWT的一方
    exp(expires): 什么时候过期,这里是一个Unix时间戳
    iat(issued at): 在什么时候签发的
   */

    /**
     * 签名秘钥
     */
    public static final String SECRET = "token";

    /**
     * 生成token
     *
     * @param id 一般传入userName
     * @return
     */
    public static String createJwtToken(String id) {
        String issuer = "GYB";
        String subject = "";
        return createJwtToken(id, issuer, subject, DEFAULT_TOKEN_TIME_MS);
    }

    public static String createJwtToken(String id, long ttlMillis) {
        String issuer = "GYB";
        String subject = "";
        return createJwtToken(id, issuer, subject, ttlMillis);
    }


    /**
     * 生成Token
     *
     * @param id        编号
     * @param issuer    该JWT的签发者,是否使用是可选的
     * @param subject   该JWT所面向的用户,是否使用是可选的;
     * @param ttlMillis 签发时间
     * @return token String
     */
    public static String createJwtToken(String id, String issuer, String subject, long ttlMillis) {

        // 签名算法 ,将对token进行签名
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 生成签发时间
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        // 通过秘钥签名JWT
        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET);
        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

        //创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
        Map<String, Object> claims = new HashMap<String, Object>();
        claims.put("userDetails", id);

        // Let's set the JWT Claims
        JwtBuilder builder = Jwts.builder().setId(id)
                .setIssuedAt(now)
                .setSubject(subject)
                .setIssuer(issuer)
                .setClaims(claims)
                .signWith(signatureAlgorithm, signingKey);

        // if it has been specified, let's add the expiration
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }

        // Builds the JWT and serializes it to a compact, URL-safe string
        return builder.compact();

    }

    // Sample method to validate and read the JWT
    public static Claims parseJWT(String jwt) {
        // This line will throw an exception if it is not a signed JWS (as expected)
        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(DatatypeConverter.parseBase64Binary(SECRET))
                    .parseClaimsJws(jwt).getBody();
            return claims;
        } catch (Exception exception) {
            return null;
        }
    }

    /**
     * 验证jwt的有效期
     *
     * @param claims
     * @return
     */
    public static Boolean isTokenExpired(Claims claims) {
        return claims == null || claims.getExpiration().before(new Date());
    }
}

五. 异常信息处理类

JwtAccessDeniedHandler

package com.securityjwtdemo.service.jwtsecurity;

import com.alibaba.fastjson.JSON;
import com.securityjwtdemo.common.JsonResponseStatus;
import com.securityjwtdemo.common.JsonResult;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

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

/**
 * @Data: 2019/10/31
 * @Des: 权限不足异常处理
 */
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        JsonResult<String> jsonResult = new JsonResult<>();
        jsonResult.setFail(JsonResponseStatus.NoRight.getCode(), "权限不足,请联系管理员 : " + accessDeniedException.getMessage());
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(jsonResult));
    }
}

JwtAuthenticationEntryPoint

package com.securityjwtdemo.service.jwtsecurity;

import com.alibaba.fastjson.JSON;
import com.securityjwtdemo.common.JsonResponseStatus;
import com.securityjwtdemo.common.JsonResult;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

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

/**
 * @Data: 2019/10/31
 * @Des:  用户权限不足处理
 */
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        JsonResult<String> jsonResult = new JsonResult<>();
        jsonResult.setFail(JsonResponseStatus.NoRight.getCode(), "权限不足 :" + authException.getMessage());
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(jsonResult));
    }
}

六. 配置类

package com.securityjwtdemo.common.config.security;

import com.securityjwtdemo.filter.security.JwtLoginFilter;
import com.securityjwtdemo.filter.security.JwtTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * @Data : 2019/10/31
 * @Des :  Spring Security 配置类
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringWebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;
    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;
    @Autowired
    private AccessDeniedHandler accessDeniedHandler;
    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;
    @Autowired
    private FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource;
    @Autowired
    private AccessDecisionManager accessDecisionManager;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 自定义登陆拦截器
        JwtLoginFilter jwtLoginFilter = new JwtLoginFilter();
        jwtLoginFilter.setAuthenticationManager(authenticationManagerBean());
        jwtLoginFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        jwtLoginFilter.setAuthenticationFailureHandler(authenticationFailureHandler);

        JwtTokenFilter jwtTokenFilter = new JwtTokenFilter();

        // 使用自定义验证实现器
        JwtAuthenticationProvider jwtAuthenticationProvider = new JwtAuthenticationProvider(userDetailsService, passwordEncoder);

        // 登陆验证信息
        http.authenticationProvider(jwtAuthenticationProvider)
                .authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setSecurityMetadataSource(filterInvocationSecurityMetadataSource);
                        object.setAccessDecisionManager(accessDecisionManager);
                        return object;
                    }
                })
                .anyRequest().authenticated()
                .and()
                .formLogin();

        // jwt 拦截器配置
        http.sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) //禁用session
                .and()
                .csrf().disable()
                .addFilterAt(jwtLoginFilter, UsernamePasswordAuthenticationFilter.class) // 添加拦截器
                .addFilterAfter(jwtTokenFilter, JwtLoginFilter.class);

        // 权限处理信息
        http.exceptionHandling()
                //   用来解决认证过的用户访问无权限资源时的异常
                .accessDeniedHandler(accessDeniedHandler)
                // 用来解决匿名用户访问无权限资源时的异常
                .authenticationEntryPoint(authenticationEntryPoint);


    }

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

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().mvcMatchers("/static/**");
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


}

七、演示

  1. lisi登陆成功(因为李四的userid是2)。李四配置的角色是Teacher,所以他访问 kw/admin权限不足,访问 jg/teacher 则可以。

Spring Security权限控制 + JWT Token 认证_第21张图片
Spring Security权限控制 + JWT Token 认证_第22张图片Spring Security权限控制 + JWT Token 认证_第23张图片
2. 张三(userid为1),则都可以访问
Spring Security权限控制 + JWT Token 认证_第24张图片
Spring Security权限控制 + JWT Token 认证_第25张图片
Spring Security权限控制 + JWT Token 认证_第26张图片
3. Token 在十分钟后失效
Spring Security权限控制 + JWT Token 认证_第27张图片

四、 其它

  • 项目地址: https://github.com/HKingFish/SpringSecurity_JWT

  • 关于添加拦截器的顺序问题,本篇放不下,所以新开一篇,详参Spring Security addFilter() 顺序问题


以上:内容部分参考:
https://www.jianshu.com/p/d5ce890c67f7
https://www.jianshu.com/p/fc56d965e3c3
https://www.jianshu.com/p/f987847cdbe3
https://www.cnblogs.com/HHR-SUN/p/7095720.html
https://segmentfault.com/a/1190000012763317
https://blog.csdn.net/dushiwodecuo/article/details/78913113
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

你可能感兴趣的:(#,Spring,Security)