Spring Security权限控制 + JWT Token 认证

Spring Security 专栏收录该内容
3 篇文章 1 订阅

一、 前言

项目实现了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安全过滤器

在这里插入图片描述

3. JWT认证

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

二、关键代码讲解

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

1. 登陆阶段流程

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

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


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

我们进去 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并返回给用户
在这里插入图片描述

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具体代码如下。
在这里插入图片描述

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

三、 关键代码

一、介绍

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

二、基础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 则可以。

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

四、 其它


以上:内容部分参考:
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
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

©️2021 CSDN 皮肤主题: 像素格子 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值