⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/mofsfely2/article/details/113756506 「mofsfely2」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

1 背景

简单先说一下需求吧,这样也好让看的人知道到底适不适合自己。

  1. 实现自定义的登录认证。
  2. 登录成功,生成token并将token 交由redis管理。
  3. 登录后对用户访问的接口进行接口级别权限认证。

springSecurity提供的注解权限校验适合的场景是系统中仅有固定的几个角色,且角色的凭证不可修改(如果修改需要改动代码)。

@PreAuthorize("hasAuthority('ROLE_TELLER')") 
public Account post(Account account, double amount);

注:ROLE_TELLER是写死的。

后端系统的访问请求有以下几种类型:

  • 登录、登出(可自定义url)
  • 匿名用户可访问的接口(静态资源,demo示例等)
  • 其他接口(在登录的前提下,继续判断访问者是否有权限访问)

2 环境搭建

依赖引入,包括springSecurity、redis、redis session需要的依赖:

<!--springSecurity安全框架-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
<!-- 默认通过SESSIONId改为通过请求头与redis配合验证session -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.3.1.RELEASE</version>
</dependency>
<!--redis支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>

注:springBoot版本也是2.3.4.RELEASE,如果有版本对应问题,自行解决。有用到swagger,为了便于测试。

新建springSecurity配置类

新建 WebSecurityConfig.java 继承自 WebSecurityConfigurerAdapter,过滤匿名用户可访问的接口。

WebSecurityConfig作为springSecurity的主配置文件。

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

/**
* Swagger等静态资源不进行拦截
*/
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/error",
"/webjars/**",
"/resources/**",
"/swagger-ui.html",
"/swagger-resources/**",
"/v2/api-docs");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//配置一些不需要登录就可以访问的接口
.antMatchers("/demo/**", "/about/**").permitAll()
//任何尚未匹配的URL只需要用户进行身份验证
.anyRequest().authenticated()
.and()
.formLogin()//允许用户进行基于表单的认证
.loginPage("/mylogin");
}

}

注:证明可以访问静态资源不会被拦截

自定义登录认证

springSecurity是基于过滤器进行安全认证的。

我们需要自定义:

  1. 登录过滤器:负责过滤登录请求,再交由自定义的登录认证管理器处理。
  2. 登录成功处理类:顾名思义,登录成功后的一些处理(设置返回信息提示“登录成功!”,返回数据类型为json)。
  3. 登录失败处理类:类似登录成功处理类。Ps:登录成功处理类和失败处理类有默认的实现可以不自定义。但是建议自定义,因为返回的信息为英文,一般情况不符合要求。
  4. 登录认证管理器:根据过滤器传过来的登录参数,进行登录认证,认证后授权。

新建登录成功处理类

需要实现 AuthenticationSuccessHandler

@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

private static final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationSuccessHandler.class);

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
//登录成功返回的认证体,具体格式在后面的登录认证管理器中
String responseJson = JackJsonUtil.object2String(ResponseFactory.success(authentication));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("登录成功!");
}
response.getWriter().write(responseJson);
}
}

新建登录失败处理类

实现 AuthenticationFailureHandler

@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

private static final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationFailureHandler.class);

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
String errorMsg;
if (StringUtils.isNotBlank(e.getMessage())) {
errorMsg = e.getMessage();
} else {
errorMsg = CodeMsgEnum.LOG_IN_FAIL.getMsg();
}
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
String responseJson = JackJsonUtil.object2String(ResponseFactory.fail(CodeMsgEnum.LOG_IN_FAIL,errorMsg));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("认证失败!");
}
response.getWriter().write(responseJson);
}

}

新建登录认证管理器

实现 AuthenticationProvider ,负责具体的身份认证(一般数据库认证,在登录过滤器过滤掉请求后传入)

@Component
public class UserVerifyAuthenticationProvider implements AuthenticationProvider {

private PasswordEncoder passwordEncoder;
@Autowired
private UserService userService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String userName = (String) authentication.getPrincipal(); // Principal 主体,一般指用户名
String passWord = (String) authentication.getCredentials(); //Credentials 网络凭证,一般指密码
//通过账号去数据库查询用户以及用户拥有的角色信息
UserRoleVo userRoleVo = userService.findUserRoleByAccount(userName);
//数据库密码
String encodedPassword = userRoleVo.getPassWord();
//credentials凭证即为前端传入密码,因为前端一般用Base64加密过所以需要解密。
String credPassword = new String(Base64Utils.decodeFromString(passWord), StandardCharsets.UTF_8);
// 验证密码:前端明文,数据库密文
passwordEncoder = new MD5Util();
if (!passwordEncoder.matches(credPassword, encodedPassword)) {
throw new AuthenticationServiceException("账号或密码错误!");
}
//ps:GrantedAuthority对认证主题的应用层面的授权,含当前用户的权限信息,通常使用角色表示
List<GrantedAuthority> roles = new LinkedList<>();
List<Role> roleList = userRoleVo.getRoleList();
roleList.forEach(role -> {
SimpleGrantedAuthority roleId = new SimpleGrantedAuthority(role.getRoleId().toString());
roles.add(roleId);
});
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userName, passWord, roles);
token.setDetails(userRoleVo);//这里可以放用户的详细信息
return token;
}

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

新建登录过滤器

LoginFilter.java继承UsernamePasswordAuthenticationFilter,负责过滤登录请求并交由登录认证管理器进行具体的认证。

public class LoginFilter extends UsernamePasswordAuthenticationFilter {

private UserVerifyAuthenticationProvider authenticationManager;

/**
* @param authenticationManager 认证管理器
* @param successHandler 认证成功处理类
* @param failureHandler 认证失败处理类
*/
public LoginFilter(UserVerifyAuthenticationProvider authenticationManager,
CustomAuthenticationSuccessHandler successHandler,
CustomAuthenticationFailureHandler failureHandler) {
//设置认证管理器(对登录请求进行认证和授权)
this.authenticationManager = authenticationManager;
//设置认证成功后的处理类
this.setAuthenticationSuccessHandler(successHandler);
//设置认证失败后的处理类
this.setAuthenticationFailureHandler(failureHandler);
//可以自定义登录请求的url
super.setFilterProcessesUrl("/myLogin");
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
//转换请求入参
UserDTO loginUser = new ObjectMapper().readValue(request.getInputStream(), UserDTO.class);
//入参传入认证管理器进行认证
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginUser.getUserName(), loginUser.getPassWord())
);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}

最后配置到WebSecurityConfig中:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserVerifyAuthenticationProvider authenticationManager;//认证用户类

@Autowired
private CustomAuthenticationSuccessHandler successHandler;//登录认证成功处理类

@Autowired
private CustomAuthenticationFailureHandler failureHandler;//登录认证失败处理类

/**
* Swagger等静态资源不进行拦截
*/
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/error",
"/webjars/**",
"/resources/**",
"/swagger-ui.html",
"/swagger-resources/**",
"/v2/api-docs");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//配置一些不需要登录就可以访问的接口
.antMatchers("/demo/**", "/about/**").permitAll()
//任何尚未匹配的URL只需要用户进行身份验证
.anyRequest().authenticated()
.and()
//配置登录过滤器
.addFilter(new LoginFilter(authenticationManager, successHandler, failureHandler))
.csrf().disable();
}

}

验证配置

访问登录请求:

成功进入LoginFilter

安全头和登录返回token

依赖已经引入了,设置session由redis存储,只需要如下图配置:

session:
store-type: redis
redis:
namespace: spring:session:admin
# session 无操作失效时间 30 分钟
timeout: 1800

设置token放入返回的header中需要在WebSecurityConfig中加入

/**
* 配置 HttpSessionIdResolver Bean
* 登录之后将会在 Response Header x-auth-token 中 返回当前 sessionToken
* 将token存储在前端 每次调用的时候 Request Header x-auth-token 带上 sessionToken
*/
@Bean
public HttpSessionIdResolver httpSessionIdResolver() {
return HeaderHttpSessionIdResolver.xAuthToken();
}

关于安全头信息可以参考:

设置安全请求头需要设置WebSecurityConfig中加入

protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//配置一些不需要登录就可以访问的接口
.antMatchers("/demo/**", "/about/**").permitAll()
//任何尚未匹配的URL只需要用户进行身份验证
.anyRequest().authenticated()
.and()
//配置登录过滤器
.addFilter(new LoginFilter(authenticationManager, successHandler, failureHandler))
.csrf().disable();
//配置头部
http.headers()
.contentTypeOptions()
.and()
.xssProtection()
.and()
//禁用缓存
.cacheControl()
.and()
.httpStrictTransportSecurity()
.and()
//禁用页面镶嵌frame劫持安全协议 // 防止iframe 造成跨域
.frameOptions().disable();
}

进行登录测试,验证结果:

注:响应中有token

查看redis。成功保存进了redis

接口权限校验

方式一:

如下图,详细请看链接。

https://blog.csdn.net/coolwindd/article/details/104640289/

注:不使用这种方式,原因在于,需要自己判断是否匿名用户。

方法二:

参考

https://blog.csdn.net/mapleleafforest/article/details/106637052

Spring Security使用FilterSecurityInterceptor过滤器来进行URL权限校验,实际使用流程大致如下:

正常情况的接口权限判断:

返回那些可以访问当前url的角色

1、定义一个MyFilterInvocationSecurityMetadataSource实现FilterInvocationSecurityMetadataSource的类,重写getAttributes方法。

方法的作用是:返回哪些角色可以访问当前url,这个肯定是从数据库中获取。要注意的是对于PathVariable传参的url,数据库中存的是这样的:/getUserByName/{name}。但实际访问的url中name是具体的值。类似的/user/getUserById也要可以匹配 /user/getUserById?1。

package com.aliyu.security.provider;/**
* @author: aliyu
* @create: 2021-02-05 14:53
* @description:
*/

import com.aliyu.service.role.RoleService;
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.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
*@author: aliyu
*@create:
*@description: 第一步:数据库查询所有权限出来:
* 之所以要所有权限,因为数据库url和实际请求url并不能直接匹配需要。比方:/user/getUserById 匹配 /user/getUserById?1
* 第二步:通过httpUrl匹配器找出允许访问当前请求的角色列表(哪些角色可以访问此请求)
*/
@Component
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

@Autowired
private RoleService roleService;

/**
* 返回当前URL允许访问的角色列表
* @param object
* @return
* @throws IllegalArgumentException
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
//入参转为HttpServletRequest
FilterInvocation fi = (FilterInvocation) object;
HttpServletRequest request = fi.getRequest();
//从数据库中查询系统所有的权限,格式为<"权限url","能访问url的逗号分隔的roleid">
List<Map<String, String>> allUrlRoleMap = roleService.getAllUrlRoleMap();
for (Map<String, String> urlRoleMap : allUrlRoleMap) {
String url = urlRoleMap.get("url");
String roles = urlRoleMap.get("roles");
//new AntPathRequestMatcher创建httpUrl匹配器:里面url匹配规则已经给我们弄好了,
// 能够支持校验PathVariable传参的url(例如:/getUserByName/{name})
// 也能支持 /user/getUserById 匹配 /user/getUserById?1
AntPathRequestMatcher matcher = new AntPathRequestMatcher(url);
if (matcher.matches(request)){ //当前请求与httpUrl匹配器进行匹配
return SecurityConfig.createList(roles.split(","));
}
}
return null;
}

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

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

注:别人是初始化的时候加载所有权限,一次就好了。我的是每次请求都会去重新加载系统所有权限,好处就是不用担心权限修改的问题。

判断当前用户是否拥有访问当前url的角色

定义一个MyAccessDecisionManager:通过实现AccessDecisionManager接口自定义一个决策管理器,判断是否有访问权限。上一步MyFilterInvocationSecurityMetadataSource中返回的当前请求可以的访问角色列表会传到这里的decide方法里面(如果没有角色的话,不会进入decide方法。

正常情况你访问的url必然和某个角色关联,如果没有关联就不应该可以访问)。decide方法传了当前登录用户拥有的角色,通过判断用户拥有的角色中是否有一个角色和当前url可以访问的角色匹配。如果匹配,权限校验通过。

package com.aliyu.security.provider;/**
* @author: aliyu
* @create: 2021-02-05 15:16
* @description:
*/
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.FilterInvocation;
import org.springframework.stereotype.Component;

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

/**
*@author: aliyu
*@create:
*@description: 接口权限判断(根据MyFilterInvocationSecurityMetadataSource获取到的请求需要的角色
* 和当前登录人的角色进行比较)
*/
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {

@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
//循环请求需要的角色,只要当前用户拥有的角色中包含请求需要的角色中的一个,就算通过。
Iterator<ConfigAttribute> iterator = configAttributes.iterator();
while(iterator.hasNext()){
ConfigAttribute configAttribute = iterator.next();
String needCode = configAttribute.getAttribute();
//获取到了登录用户的所有角色
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (StringUtils.equals(authority.getAuthority(), needCode)) {
return;
}
}
}
throw new AccessDeniedException("当前访问没有权限");
}

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

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

处理匿名用户访问无权限资源

1、定义一个CustomAuthenticationEntryPoint实现AuthenticationEntryPoint处理匿名用户访问无权限资源(可以理解为未登录的用户访问,确实有些接口是可以不登录也能访问的,比较少,我们在WebSecurityConfig已经配置过了。如果多的话,需要另外考虑从数据库中获取,并且权限需要加一个标志它为匿名用户可访问)。

package com.aliyu.security.handler;

import com.aliyu.common.util.JackJsonUtil;
import com.aliyu.entity.common.vo.ResponseFactory;
import com.aliyu.security.constant.MessageConstant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

import static com.aliyu.entity.common.exception.CodeMsgEnum.MOVED_PERMANENTLY;

/**
* 未登录重定向处理器
* <p>
* 未登录状态下访问需要登录的接口
*
* @author
*/
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

private static final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationEntryPoint.class);


@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
//原来不需要登录的接口,现在需要登录了,所以叫永久移动
String message = JackJsonUtil.object2String(
ResponseFactory.fail(MOVED_PERMANENTLY, MessageConstant.NOT_LOGGED_IN)
);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("未登录重定向!");
}
response.getWriter().write(message);
}

}

处理登陆认证过的用户访问无权限资源

2、定义一个CustomAccessDeniedHandler 实现AccessDeniedHandler处理登陆认证过的用户访问无权限资源。

package com.aliyu.security.handler;

import com.aliyu.common.util.JackJsonUtil;
import com.aliyu.entity.common.exception.CodeMsgEnum;
import com.aliyu.entity.common.vo.ResponseFactory;
import com.aliyu.security.constant.MessageConstant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;


/**
* 拒绝访问处理器(登录状态下,访问没有权限的方法时会进入此处理器)
*
* @author
*/
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

private static final Logger LOGGER = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);

@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
String message = JackJsonUtil.object2String(
ResponseFactory.fail(CodeMsgEnum.UNAUTHORIZED, MessageConstant.NO_ACCESS)
);
if(LOGGER.isDebugEnabled()){
LOGGER.debug("没有权限访问!");
}
response.getWriter().write(message);
}


}

配置到WebSecurityConfig上面去

@Autowired
private MyFilterInvocationSecurityMetadataSource securityMetadataSource;//返回当前URL允许访问的角色列表
@Autowired
private MyAccessDecisionManager accessDecisionManager;//除登录登出外所有接口的权限校验
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(accessDecisionManager);
object.setSecurityMetadataSource(securityMetadataSource);
return object;
}
})
//用来解决匿名用户访问无权限资源时的异常
.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
//用来解决登陆认证过的用户访问无权限资源时的异常
.accessDeniedHandler(new CustomAccessDeniedHandler())

完整的Java类:

package com.aliyu.security.config;

import com.aliyu.filter.LoginFilter;
import com.aliyu.security.handler.*;
import com.aliyu.security.provider.MyAccessDecisionManager;
import com.aliyu.security.provider.MyFilterInvocationSecurityMetadataSource;
import com.aliyu.security.provider.UserVerifyAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.session.web.http.HeaderHttpSessionIdResolver;
import org.springframework.session.web.http.HttpSessionIdResolver;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserVerifyAuthenticationProvider authenticationManager;//认证用户类

@Autowired
private CustomAuthenticationSuccessHandler successHandler;//登录认证成功处理类

@Autowired
private CustomAuthenticationFailureHandler failureHandler;//登录认证失败处理类

@Autowired
private MyFilterInvocationSecurityMetadataSource securityMetadataSource;//返回当前URL允许访问的角色列表
@Autowired
private MyAccessDecisionManager accessDecisionManager;//除登录登出外所有接口的权限校验

/**
* 密码加密
* @return
*/
@Bean
@ConditionalOnMissingBean(PasswordEncoder.class)
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

/**
* 配置 HttpSessionIdResolver Bean
* 登录之后将会在 Response Header x-auth-token 中 返回当前 sessionToken
* 将token存储在前端 每次调用的时候 Request Header x-auth-token 带上 sessionToken
*/
@Bean
public HttpSessionIdResolver httpSessionIdResolver() {
return HeaderHttpSessionIdResolver.xAuthToken();
}
/**
* Swagger等静态资源不进行拦截
*/
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/error",
"/webjars/**",
"/resources/**",
"/swagger-ui.html",
"/swagger-resources/**",
"/v2/api-docs");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//配置一些不需要登录就可以访问的接口
.antMatchers("/demo/**", "/about/**").permitAll()
//任何尚未匹配的URL只需要用户进行身份验证
.anyRequest().authenticated()
//登录后的接口权限校验
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(accessDecisionManager);
object.setSecurityMetadataSource(securityMetadataSource);
return object;
}
})
.and()
//配置登出处理
.logout().logoutUrl("/logout")
.logoutSuccessHandler(new CustomLogoutSuccessHandler())
.clearAuthentication(true)
.and()
//用来解决匿名用户访问无权限资源时的异常
.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
//用来解决登陆认证过的用户访问无权限资源时的异常
.accessDeniedHandler(new CustomAccessDeniedHandler())
.and()
//配置登录过滤器
.addFilter(new LoginFilter(authenticationManager, successHandler, failureHandler))
.csrf().disable();
//配置头部
http.headers()
.contentTypeOptions()
.and()
.xssProtection()
.and()
//禁用缓存
.cacheControl()
.and()
.httpStrictTransportSecurity()
.and()
//禁用页面镶嵌frame劫持安全协议 // 防止iframe 造成跨域
.frameOptions().disable();
}
}

3其他

测试结果我就懒得写了,就这样了。

特别的,我们认为如果一个接口属于当前系统,那么它就应该有对应可以访问的角色。这样的接口才会被我们限制住。如果一个接口只是在当前系统定义了,而没有指明它的角色,这样的接口是不会被我们限制的。

注意点

下面的代码,本意是想配置一些不需要登录也可以访问的接口。

但是测试的时候发现,任何接口的调用都会进入这里MyFilterInvocationSecurityMetadataSource getAttriButes方法,包括我webSecurityConfig里配置的不需要登录的url。结果就是不需要登录的url和没有配置角色的接口权限一样待遇,要么都能访问,要么都不能访问!!!

所以如上图,我在这里配置了不需要登录的接口(因为不知道如何从webSercurityConfig中获取,干脆就配置在这里了),去掉了webSercurityConfig中的相应配置。

文章目录
  1. 1. 1 背景
  2. 2. 2 环境搭建
    1. 2.1. 新建springSecurity配置类
    2. 2.2. 自定义登录认证
      1. 2.2.0.1. 新建登录成功处理类
      2. 2.2.0.2. 新建登录失败处理类
      3. 2.2.0.3. 验证配置
  3. 2.3. 安全头和登录返回token
  4. 2.4. 接口权限校验
    1. 2.4.0.1. 返回那些可以访问当前url的角色
    2. 2.4.0.2. 判断当前用户是否拥有访问当前url的角色
    3. 2.4.0.3. 处理匿名用户访问无权限资源
    4. 2.4.0.4. 处理登陆认证过的用户访问无权限资源
  • 3. 3其他
    1. 3.1. 注意点