文章目录
一、 前言
项目实现了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.我们在配置类中添加了两个自定义拦截器 JwtLoginFilter
和 JwtTokenFilter
。我们这里关注 JwtLoginFilter
2.JwtLoginFilter
继承了 UsernamePasswordAuthenticationFilter
, UsernamePasswordAuthenticationFilter
是用来处理身份验证的表单提交。也就是说 我们在 JwtLoginFilter
中处理表单提交的身份信息。
我们进去 UsernamePasswordAuthenticationFilter
可以看到在其构造函数指定了拦截路径,即默认拦截 Post 请求方式的 /login 请求。我们可以在配置类中通过 formLogin().loginProcessingUrl(“XXXX”)
来指定登陆路径。
3. 在 JwtLoginFilter
中,我们获取参数username,password 来获取提交的用户名和密码,封装了凭证后进行登陆信息的校验。this.getAuthenticationManager()
来获取用户认证的管理类 。用户认证的管理类,所有的认证请求(比如login)都会通过提交一个token给AuthenticationManager
的authenticate()
方法来实现。当然事情肯定不是它来做,具体校验动作会由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.由于我们在拦截链中加入了JwtLoginFilter
、JwtTokenFilter
。而 JwtLoginFilter
上面说过只拦截登陆路径。其余路径则会被 JwtTokenFilter
拦截。
2. JwtTokenFilter
具体代码如下。
- 如果验证通过,则将token保存在Security上下文中。并进行下一步调用。
- 用户的请求会到达
JwtFilterInvocationSecurityMetadataSource
中。JwtFilterInvocationSecurityMetadataSource
根据当前路径获取到有资格访问当前页面的角色列表(比如 Admin,Teacher 等)。
- 随后调用链路到了
JwtUrlAccessDecisionManager
中,在这里来校验当前用户是否具备所需要的角色。校验通过,则允许访问,否则抛出 AccessDeniedException 异常。
- JwtAccessDeniedHandler 返回错误信息,这里没有返回403错误。而是算他访问成功,提示权限不足。具体根据业务调整
三、 关键代码
一、介绍
- 数据库的设置,本项目中其实只是使用了elst_menu,else_role,elst_user 以及其关联表,elst_user_role, elst_role_menu。
- 一个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();
}
}
七、演示
- lisi登陆成功(因为李四的userid是2)。李四配置的角色是Teacher,所以他访问 kw/admin权限不足,访问 jg/teacher 则可以。
2. 张三(userid为1),则都可以访问
3. Token 在十分钟后失效
四、 其它
-
关于添加拦截器的顺序问题,本篇放不下,所以新开一篇,详参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
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正