涉及到的文件介绍
AjaxAccessDeniedHandler----用户权限不足时反给前端的数据
AjaxAuthenticationEntryPoint----用户没登陆时反给前端的数据
JwtAuthenticationTokenFilter----Jwt过滤器(第一个过滤器):获取用户token,查询用户信息拼装到security中,以便后续filter使用
JwtUtils----Jwt工具包
SpringSecurityConfig----Security核心配置文件
UrlAccessDecisionManager----自定义的比较逻辑;根据用户信息和权限去与当前访问的url需要的权限进行对比
UrlFilterInvocationSecurityMetadataSource----这个类是分析得出 用户访问的 url 需要哪些权限
其它关于权限模块的表和bean类我就不记了,用脚都能写出来。
下面是根据用户Id获取用户权限的sql:
SELECT a.permission FROM sys_resource a WHERE a.id in (
SELECT resource_id FROM sys_role_resource WHERE role_id in (
SELECT role_id FROM sys_user_role WHERE user_id = #{userId}
)
)
下面是具体实现代码,无冗余代码,JwtAuthenticationTokenFilter中第40行的方法就是上面那条SQL
AjaxAccessDeniedHandler
import com.alibaba.fastjson.JSON;
import com.sonice1024.hongchen.common.response.ResponseCode;
import com.sonice1024.hongchen.common.response.ResponseResult;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 用户权限不足时反给前端的数据
*/
@Component
public class AjaxAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException {
ResponseResult result = ResponseResult.build(ResponseCode.NO_AUTHORITY);
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setHeader("Content-Type", "application/json");
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
AjaxAuthenticationEntryPoint
import com.alibaba.fastjson.JSON;
import com.sonice1024.hongchen.common.response.ResponseCode;
import com.sonice1024.hongchen.common.response.ResponseResult;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 用户没登陆时反给前端的数据
*/
@Component
public class AjaxAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {
ResponseResult result = ResponseResult.build(ResponseCode.TOKEN_FAIL);
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setHeader("Content-Type", "application/json");
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
JwtUtils
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang3.StringUtils;
import java.util.Date;
/**
* Jwt工具包
*/
public class JwtUtils {
private static final String SUBJECT = "***";
private static final long EXPIRE = 1000*60*60*24*3; //过期时间,毫秒,天
// 秘钥
private static final String APPSECRET = "***";
/**
* 生成jwt的token串
* @param username 用户名
* @return String token字符串
*/
public static String createJwtToken(String username){
if(StringUtils.isBlank(username)){
return null;
}
String token = Jwts.builder().setSubject(SUBJECT)
.claim("username", username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis()+EXPIRE))
.signWith(SignatureAlgorithm.HS512,APPSECRET)
.compact();
return token;
}
/**
* 从token中获取用户名
* @param token token字符串
* @return Steing 用户名
*/
public static String getUsername(String token){
try {
// 他自己会判断过期时间
final Claims claims = Jwts.parser().setSigningKey(APPSECRET).parseClaimsJws(token).getBody();
if (claims != null) return (String) claims.get("username");
} catch (Exception e) {
return null;
}
return null;
}
}
JwtAuthenticationTokenFilter
import com.sonice1024.hongchen.entity.SysUser;
import com.sonice1024.hongchen.service.SysUserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
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;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* Jwt过滤器(第一个过滤器):获取用户token,查询用户信息拼装到security中,以便后续filter使用
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private SysUserService sysUserService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
// 获取token
String tokenStr = request.getHeader("Authorization");
if (StringUtils.isNotBlank(tokenStr)) {
// 获取用户名
String tokenObj = JwtUtils.getUsername(tokenStr);
if (StringUtils.isNotBlank(tokenObj)) {
// 查询用户信息和权限列表
SysUser user = sysUserService.getAUserAndPermission(tokenObj);
if (user != null) {
// 将权限列表给security,以便后续filter使用
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
if (user.getPermissions() != null && user.getPermissions().size() > 0) {
authorities = user.getPermissions().stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
//设置当前上下文的认证信息
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(tokenObj, "", authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
//调用下一个过滤器
chain.doFilter(request, response);
}
}
UrlFilterInvocationSecurityMetadataSource
import com.sonice1024.hongchen.entity.SysResource;
import com.sonice1024.hongchen.service.SysResourceService;
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 javax.annotation.Resource;
import java.util.Collection;
/**
* URL过滤器(第二个过滤器):
* 1. 这个类是分析得出 用户访问的 url 需要哪些权限
* 2. 核心的方法是第一个
* 3. 第三个方法返回true表示支持支持这种方式即可
*/
@Component
public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Resource
private SysResourceService sysResourceService;
/**
* 在用户发出请求时,根据请求的url查出该url需要哪些权限才能访问,并将所需权限给SecurityConfig
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
// 获取 请求 url 地址
String requestUrl = ((FilterInvocation) o).getRequestUrl();
// 得到所请求的 url 和 资源权限 的对应关系(这里可以用缓存处理)
SysResource resource = sysResourceService.getPermissionByUrl(requestUrl);
if (resource != null && resource.getVerification() == 1) {
return SecurityConfig.createList(resource.getPermission());
} else {
// 如果都没有匹配上,我们返回默认值,这个值就像一个特殊的标识符,自定义,在UrlAccessDecisionManager中自定义规则即可
return SecurityConfig.createList("ROLE_ANONYMOUS");
}
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
UrlAccessDecisionManager
import org.springframework.http.HttpMethod;
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.security.core.context.SecurityContextHolder;
import org.springframework.security.web.FilterInvocation;
import org.springframework.stereotype.Component;
import java.util.Collection;
/**
* 根据用户信息和权限去与当前访问的url需要的权限进行对比
*/
@Component
public class UrlAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
//放行options请求
FilterInvocation fi = (FilterInvocation) o;
if (HttpMethod.OPTIONS.name().equals(fi.getRequest().getMethod())) {
return;
}
// collection是url过滤器给过来的权限列表,判断url是否需要拦截
for (ConfigAttribute attribute : collection) {
if (!"ROLE_ANONYMOUS".equals(attribute.toString())) { // 需要拦截的
// 看是否有用户信息
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal == null) throw new AccessDeniedException("expire");
// 看权限是否足够
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
// authority.getAuthority()是jwt过滤器给过来的权限
// attribute.getAttribute()是url过滤器给过来的权限
if (authority.getAuthority().equals(attribute.getAttribute())) {
return;
}
}
throw new AccessDeniedException("not allow");
}
}
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
SpringSecurityConfig
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
AjaxAuthenticationEntryPoint authenticationEntryPoint; // 未登陆时返回 JSON 格式的数据给前端(否则为 html)
@Autowired
AjaxAccessDeniedHandler accessDeniedHandler; // 无权访问返回的 JSON 格式数据给前端(否则为 403 html 页面)
@Autowired
private JwtAuthenticationTokenFilter jwtRequestFilter; // Jwt拦截器,获取用户的数据和用户拥有的权限
@Autowired
UrlAccessDecisionManager urlAccessDecisionManager; // 自定义的权限比较器
@Autowired
UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource; // 获取访问URL需要的权限
/**
* 配置说明:
* 1。 因为整个系统是前后端分离的,不需要Security自带的登陆和退出登陆功能,所以不需要配置.formLogin()和.logout();
* 如果我们使用session的方式记录用户登陆状态就需要在“登陆接口”将session保存到内存中,后面的授权Security会自己去拿session;
* 如果用token方式,在“登陆接口”我们需要生成token给客户端,客户端在登陆后的其它请求中需要将token放在header中传入接口;
* 默认是使用session保存用户登陆的状态的,我们需要改为使用token的模式:
* 1. 用户访问接口,首先进入JwtAuthenticationTokenFilter,我们需要在这里面构建用户信息,包括用户有哪些权限
* 2. 经过JwtAuthenticationTokenFilter,得到用户有哪些权限后,进入UrlFilterInvocationSecurityMetadataSource获取用户
* 访问的url是否需要拦截
* 3. 所有的数据都有了,在UrlAccessDecisionManager中自定义比较即可
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()//不使用防跨站攻击
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//不使用session
.and().authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // OPTIONS放行
.anyRequest().authenticated() // 其它的全部自定义验证
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setAccessDecisionManager(urlAccessDecisionManager);
o.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource);
return o;
}
});
http.headers().cacheControl();//http的cache控制,如下这句代码会禁用cache
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);//添加JWT身份认证的filter
//添加自定义未授权的处理器
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
//添加自定义未登录的处理器
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
}
}
有兴趣交流的可以关注公众号或加我的QQ群: