Spring Security权限控制 + JWT Token 认证

1、 前言

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

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安全过滤器java

在这里插入图片描述

3. JWT认证

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

2、关键代码讲解

文末会给出项目地址,因此基础搭建不在赘述。github

1. 登录阶段流程

1. 登录阶段流程图。
中间省略了Spring Security 的某些调用。仅用来描绘本身代码的逻辑。
在这里插入图片描述
2. 登录阶段讲解。(请自觉忽略个人背景图。。。。。)web

1.咱们在配置类中添加了两个自定义拦截器 JwtLoginFilterJwtTokenFilter 。咱们这里关注 JwtLoginFilter
在这里插入图片描述算法


2.JwtLoginFilter 继承了 UsernamePasswordAuthenticationFilterUsernamePasswordAuthenticationFilter 是用来处理身份验证的表单提交。也就是说 咱们在 JwtLoginFilter 中处理表单提交的身份信息。在这里插入图片描述spring

咱们进去 UsernamePasswordAuthenticationFilter 能够看到在其构造函数指定了拦截路径,即默认拦截 Post 请求方式的 /login 请求。咱们能够在配置类中经过 formLogin().loginProcessingUrl(“XXXX”) 来指定登录路径。
在这里插入图片描述
3. 在 JwtLoginFilter 中,咱们获取参数username,password 来获取提交的用户名和密码,封装了凭证后进行登录信息的校验。this.getAuthenticationManager() 来获取用户认证的管理类 。用户认证的管理类,全部的认证请求(好比login)都会经过提交一个token给AuthenticationManagerauthenticate()方法来实现。固然事情确定不是它来作,具体校验动做会由AuthenticationManager将请求转发给具体的实现类来作。咱们这里的实现类即 JwtAuthenticationProvider
在这里插入图片描述
4. 跳转到 JwtAuthenticationProvider.authenticate 中进行逻辑处理(由于在配置类中指定了经过 authenticationProvider方法配置了校验类)。 JwtAuthenticationProvider.authenticate 的具体校验,根据注释就能够清楚了。
在这里插入图片描述
5. 校验成功则会调用 JwtLoginSuccessHandler.生成一个token并返回给用户
在这里插入图片描述sql

5.失败则会调用 JwtLoginFailureHandler 返回错误信息
在这里插入图片描述
6. 至于为何会调用这两个类,是由于咱们在配置类中进行了初始化配置:
在这里插入图片描述
而且在拦截链路中加入了这两个拦截器。
对于添加拦截器如下三个方法:数据库

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

在这里插入图片描述

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

1. Token验证流程图
在这里插入图片描述
2. 代码讲解
1.因为咱们在拦截链中加入了JwtLoginFilterJwtTokenFilter 。而 JwtLoginFilter 上面说过只拦截登录路径。其他路径则会被 JwtTokenFilter 拦截。
在这里插入图片描述
2. JwtTokenFilter具体代码以下。
在这里插入图片描述json

  1. 若是验证经过,则将token保存在Security上下文中。并进行下一步调用。
    在这里插入图片描述
  2. 用户的请求会到达 JwtFilterInvocationSecurityMetadataSource 中。 JwtFilterInvocationSecurityMetadataSource 根据当前路径获取到有资格访问当前页面的角色列表(好比 Admin,Teacher 等)。
    在这里插入图片描述
  3. 随后调用链路到了 JwtUrlAccessDecisionManager 中,在这里来校验当前用户是否具有所须要的角色。校验经过,则容许访问,不然抛出 AccessDeniedException 异常。
    6. 在这里插入图片描述
    在这里插入图片描述
  4. JwtAccessDeniedHandler 返回错误信息,这里没有返回403错误。而是算他访问成功,提示权限不足。具体根据业务调整
    在这里插入图片描述

3、 关键代码

1、介绍

  1. 数据库的设置,本项目中其实只是使用了elst_menu,else_role,elst_user 以及其关联表,elst_user_role, elst_role_menu。
  2. 一个User具备多个Role,一个Role对应能够访问多个Menu。
    在这里插入图片描述

2、基础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);
    }
}

3、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>

4、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();
    }


}

7、演示

  1. lisi登录成功(由于李四的userid是2)。李四配置的角色是Teacher,因此他访问 kw/admin权限不足,访问 jg/teacher 则能够。

在这里插入图片描述
在这里插入图片描述在这里插入图片描述
2. 张三(userid为1),则均可以访问
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
3. Token 在十分钟后失效
在这里插入图片描述

4、 其它

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

关于添加拦截器的顺序问题,本篇放不下,因此新开一篇,点击查看哦!


以上:内容部分参考:
https://www.jianshu.com/p/d5ce890c67f7
https://www.jianshu.com/p/fc56d965e3c3
https://www.jianshu.com/p/f987847cdbe3
http://www.javashuo.com/article/p-hxvyosau-kd.html
http://www.javashuo.com/article/p-peiemeoh-x.html
http://www.javashuo.com/article/p-rgyffmcv-nn.html
若有侵扰,联系删除。 内容仅用于自我记录学习使用。若有错误,欢迎指正