SpingSecurity的那些事
一、什么是springSecurity
Spring Security是一个框架,致力于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring Security的真正强大之处在于可以轻松扩展以满足自定义要求
因此其核心就有两块,1:认证,2:授权
二、一些概念性解释
-
认证管理器(AuthenticationManager),认证管理器下有许多认证器(AuthenticationProvider),具体的认证逻辑由AuthenticationProvider实现提供。
-
决策管理器(AccessDecisionManager),里面由许多投票器,默认为WebExpressionVoter,由投票器进行投票。
-
过滤链(Filter):每个过滤链都实现一些功能而且也是扩展点的入口:
因此要学习springSecurity,首先要知道他有那些过滤链,这些过滤链都实现了什么功能,整明白过滤链,就能自己实现自定义的过滤功能了。结合上图,这里有一些过滤链的解释:
3.1. WebAsyncManagerIntegrationFilter: 将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。
3.2. SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的。
3.3. HeaderWriterFilter:用于将头信息加入响应中。
3.4. CsrfFilter:用于处理跨站请求伪造。
3.5. LogoutFilter:用于处理退出登录。
3.6. UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login
的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username
和 password
,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。
3.7. DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
3.8. BasicAuthenticationFilter:检测和处理 http basic 认证。
3.9. RequestCacheAwareFilter:用来处理请求的缓存。
3.10. SecurityContextHolderAwareRequestFilter:主要是包装请求对象request
3.11. AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。
3.12. SessionManagementFilter:管理 session 的过滤器
3.13. ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。
3.14. FilterSecurityInterceptor:可以看做过滤器链的出口,也是在这一步进行策略投票表决的。其使用的决策器默认为AffirmativeBased,默认的投票器为WebExpressionVoter。
3.15. RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。
三、配置详解
由于springSecurity在实现是是一条过滤链,springSecurity将所有的配置都集中的WebSecurityConfigurerAdapter中进行配置管理,WebSecurityConfigurerAdapter中提供了一个配置入口方法:
下面是一些 常用的http. 配置项的详解:
- .authorizeRequests() //url 匹配规则:
- .requestMatchers() 配置一个request Mather数组,参数为
- .RequestMatcher 对象,其match 规则自定义,需要的时候放在最前面,对需要匹配的的规则进行自定义与过滤
- .authorizeRequests() URL权限配置
- .antMatchers() 配置一个request Mather 的 string数组,参数为 ant 路径格式, 直接匹配url
- .anyRequest 匹配任意url,无参 ,最好放在最后面
- .authorizeRequests() //url保护配置
- .authenticated() 保护UrL,需要用户登录
- .permitAll() 指定URL无需保护,一般应用与静态资源文件
- .hasRole(String role) 限制单个角色访问,角色将被增加 “ROLE_” .所以”ADMIN” 将和 “ROLE_ADMIN”进行比较. 另一个方法是hasAuthority(String authority)
- .hasAnyRole(String… roles) 允许多个角色访问. 另一个方法是hasAnyAuthority(String… authorities)
- .access(String attribute) 该方法使用 SPEL, 所以可以创建复杂的限制 例如如access(“permitAll”), access("hasRole(‘ADMIN’)
- .and hasIpAddress(‘123.123.123.123’)")
- .hasIpAddress(String ipaddressExpression) 限制IP地址或子网
- .formLogin() //基于表单登录的配置
- .formLogin() 基于表单登录
- .loginPage() 登录页
- .defaultSuccessUrl 登录成功后的默认处理页
- .failuerHandler登录失败之后的处理器
- .successHandler登录成功之后的处理器
- .failuerUrl登录失败之后系统转向的url,默认是this.loginPage + “?error”
- logout() //登出logout配置
- .logoutUrl 登出url , 默认是/logout, 它可以是一个ant path url
- .logoutSuccessUrl 登出成功后跳转的 url 默认是"/login?logout"
- .logoutSuccessHandler 登出成功处理器,设置后会把logoutSuccessUrl 置为null
- .exceptionHandling() //异常处理配置
- .accessDeniedHandler 授权异常处理
- .authenticationEntryPoint 认证异常的处理点
- .accessDeniedPage :设置被拒绝访问的错误页面(比如 /errors/401) accessDeniedUrl : 1. 如果 accessDeniedUrl 为 null,则返回 403 给浏览器端 2. 如果 accessDeniedUrl 不为 null,是某个 / 开头的有效路径,则 foward 用户到相应的错误页面
- .defaultAccessDeniedHandlerFor 设置一个默认的授权异常处理该处理程序首选为所提供的RequestMatcher调用
- .defaultAuthenticationEntryPointFor 设置默认的认证异常处理,其将优先被RequestMatcher调用
- .sessionManagement() 配置
- .invalidSessionUrl 设置session id无效时的跳转URL。如果设置了该属性,浏览器端提供了无效的session id时,服务器端会将其跳转到所设置的URL。
- .invalidSessionStrategy 设置session id无效时要应用的策略InvalidSessionStrategy。如果设置了该属性,浏览器端提供了无效的session id时,服务器端会调用该策略对象。Security内置地对该接口仅提供了一个实现就是SimpleRedirectInvalidSessionStrategy。
- .invalidSessionStrategy和#invalidSessionUrl都被调用时,#invalidSessionStrategy会生效;
- .sessionAuthenticationErrorUrl 定义session异常处理抛出异常时要跳转的URL。如果未设置该属性,SessionAuthenticationStrategy抛出异常时,会返回402给客户端。
- .sessionAuthenticationFailureHandler 定义session认证抛出异常时要应用的认证失败处理器AuthenticationFailureHandler。如果未设置该属性,SessionAuthenticationStrategy抛出异常时,会返回402给客户端。
- .enableSessionUrlRewriting true,允许将HTTP session信息重写到URL中。该方法对应的属性enableSessionUrlRewriting缺省为false,不允许Http session重写到URL。
- .sessionCreationPolicy 设置会话创建策略
- .sessionAuthenticationStrategy 允许设置一个会话认证策略。如果不设置,会使用缺省值。默认是SessionFixationProtectionStrategy
- .maximumSessions 设置每个用户的最大并发会话数量。此方法返回一个ConcurrencyControlConfigurer,这也是一个安全配置器,设置每个用户会话数量超出单用户最大会话并发数时如何处理。ConcurrencyControlConfigurer的配置能力如下
9.1 .expiredUrl 设置一个URL。如果某用户达到单用户最大会话并发数后再次请求新会话,则将最老的会话超时并将其跳转到该URL。
9.2 .expiredSessionStrategy 设置一个会话信息超时策略对象。如果某用户达到单用户最大会话并发数后再次请求新会话,则调用该策略超时哪个会话以及进行什么样的超时处理。
9.3 .maxSessionsPreventsLogin 设置属性maxSessionsPreventsLogin.如果设置为true,则某用户达到单用户最大会话并发数后再次请求登录时会被拒绝登录。
缺省情况下maxSessionsPreventsLogin为false。则某用户达到单用户最大会话并发数后再次请求登录时,其最老会话会被超时并被重定向到#expiredUrl所设置的URL(或者被#expiredSessionStrategy所设置策略处理)。- .sessionRegistry 设置所要使用的SessionRegistry,不设置时的缺省值为一个SessionRegistryImpl。
.sessionFixation 方法返回一个SessionFixationConfigurer,这也是一个安全配置器,专门对Session Fixcation保护机制做出设置。
SessionFixationConfigurer的配置能力如下
10.1 .newSession 设置固定会话攻击保护策略为SessionFixationProtectionStrategy,该策略会在用户会话认证成功时创建新的会话,但不会复制旧会话的属性。
10.2 .migrateSession 设置固定会话攻击保护策略为SessionFixationProtectionStrategy,该策略会在用户会话认证成功时创建新的会话,并且复制旧会话的属性。
10.3 .changeSessionId 设置固定会话攻击保护策略为ChangeSessionIdAuthenticationStrategy,仅针对Servlet 3.1+,在用户会话认证成功时调用Servlet时变更会话ID并保留所有会话属性
- .addFilter 添加过滤器Filter
- addFilter 添加过滤器
- .addFilterAfter() 在指定的过滤器之后添加新的过滤器
- .addFilterBefore()在指定的过滤器之间添加新的过滤器
- .addFilterAt();在 atFilter 相同位置添加 filter, 此 filter 不覆盖 filter
三、自定义过滤器(实现自定义登录)
上面的一些概念性解释中已经大致解释了一下过滤链,其中有个UsernamePasswordAuthenticationFilter:的过滤链,这个事官方默认提供的对/login ,post请求的访问进行拦截。首先先对源码进行分析。其实现逻辑大致分析图如下:
因此我们需要继承父类AbstractAuthenticationProcessingFilter,并在里面注入认证管理器:ProviderManager,并在认证管理的认证器列表中添加一个认证器AuthenticationProvider,最后在认证器中注入获取用户信息和密码加解密等对象即可。
我的代码如下:
代码太多我只贴放了主体结构代码,具体的实现还需根据自己需要,弄懂认证过滤链,认证器管理器,认证器的关系,代码就好写了。
- 过滤链实现类:
```java
@Component
public class MyUsernamePwdAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final String KEY_USERNAME = "username";
private static final String KEY_PASSWORD = "password";
private static final String KEY_VALIDATECODE = "validateCode";
private static final String KEY_VALIDATE_TYPE = "validateType";
//提供一个默认值,可通过配置文件更改
/**用户名*/
private String usernameParameter = KEY_USERNAME;
/**用户密码*/
private String passwordParameter = KEY_PASSWORD;
/**验证码*/
private String validateCodeParameter = KEY_VALIDATECODE;
/**是否需要验证码校验 (默认不需要,1需要,0不需要)*/
private String isValidate = "1";
/**仅支持post提交标识*/
private boolean postOnly = true;
/**
* 自定义登录成功处理器
*/
@Autowired
private UserLoginSuccessHandler userLoginSuccessHandler;
/**
* 自定义登录失败处理器
*/
@Autowired
private UserLoginFailureHandler userLoginFailureHandler;
/**
* 自定义的认证管理器
*/
@Autowired
private AuthenticationManager myProviderManager;
public MyUsernamePwdAuthenticationFilter() {
// 设置默认的拦截的请求路径和提交方式
super(new AntPathRequestMatcher("/loginSvc/login.json", "POST"));
}
@Override
public void afterPropertiesSet() {
// 填充父类中的一些私有属性
super.setAuthenticationManager(myProviderManager);
// 初始化bean时,重写父类中登录成功,登录失败的异常处理对象
super.setAuthenticationFailureHandler(userLoginFailureHandler);
super.setAuthenticationSuccessHandler(userLoginSuccessHandler);
super.afterPropertiesSet();
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException( "MyUsernamePwdAuthenticationFilter method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
String validateCode = obtainPassword(request);
String validateType = request.getParameter(KEY_VALIDATE_TYPE);
HttpSession session = request.getSession();
if(StringUtils.isBlank(username.trim()) || StringUtils.isBlank(password.trim())){
throw new AuthenticationServiceException( "MyUsernamePwdAuthenticationFilter 参数不能为空!");
}
if(StringUtils.isNotBlank(validateType) && isValidate.equals(validateType)){
String sessioId =session.getId();
//验证码判断
if(!VerificationCodeUtils.isRight(validateCode,sessioId)){
throw new AuthenticationServiceException( ExcepEnum.ERROR_12_03.getExcepDes());
}
}
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(passwordParameter);
}
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
}
protected void setDetails(HttpServletRequest request,
UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}
public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getUsernameParameter() {
return usernameParameter;
}
public final String getPasswordParameter() {
return passwordParameter;
}
- 自定义认证管理器
```java
/**
* @author: liukj
* @date: 2020/1/17
* @description: 自定义认证管理器
*/
@Component("myProviderManagerConfig")
public class MyProviderManager implements InitializingBean {
private MyProviderManager(){}
@Autowired
private MyAuthProvider myAuthProvider;
private List<AuthenticationProvider> providerList = new ArrayList<>(10);
@PostConstruct
public void init(){
this.getProviderList().add(myAuthProvider);
}
/**
* 产生一个认证管理器
* @return
*/
@Bean(name = "myProviderManager")
public AuthenticationManager generateProviderManager(){
AuthenticationManager myProviderManager = new ProviderManager(this.getProviderList());
return myProviderManager;
}
public List<AuthenticationProvider> getProviderList() {
return providerList;
}
public void setProviderList(List<AuthenticationProvider> providerList) {
this.providerList = providerList;
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.notEmpty(this.getProviderList(),"认证管理器中必须指定一个认证器!");
}
}
- 自定义认证器
/**
* @author: liukj
* @date: 2020/1/17
* @description: 自定义认证器
*/
@Component("myAuthProvider")
public class MyAuthProvider extends DaoAuthenticationProvider {
/**
* 密码加解密服务
*/
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 加载用户详细信息服务
*/
@Autowired
@Qualifier("userDetailServiceImpl")
private UserDetailsService userDetailsService;
/**
* 修改用户密码服务
*/
@Autowired
@Qualifier("userDetailsPasswordServiceImpl")
private UserDetailsPasswordService userDetailsPasswordService;
/**
* 会在bean初始化完成之前由BeanInitialization接口中的 afterPropertiesSet调用,
* 在这里重写父类中的密码加密、用户信息加载等对象
* @throws Exception
*/
@Override
protected void doAfterPropertiesSet() throws Exception {
super.setPasswordEncoder(passwordEncoder);
super.setUserDetailsPasswordService(userDetailsPasswordService);
super.setUserDetailsService(userDetailsService);
super.doAfterPropertiesSet();
}
}
- 密码加解密判断
/**
* @author: liukj
* @date: 2019/12/17
* @description: 密码加密和判断
*/
@Component
public class PasswordEncoderImpl implements PasswordEncoder {
private static final Logger logger = LoggerFactory.getLogger(PasswordEncoderImpl.class);
/**
* 密码的加密
* @param rawPassword 明文
* @return 密文
*/
@Override
public String encode(CharSequence rawPassword) {
String encryptText = EncryptOfMd5Utils.md5DigestAsHex(rawPassword.toString());
return encryptText;
}
/**
* 明文和密文的对比
*
* @param rawPassword 明文
* @param encodedPassword 加密后的密文
* @return true 密码相等,否正不相等
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if(null == rawPassword || rawPassword.length()==0){
logger.warn("Empty encoded password!");
return false;
}
String encryptText = EncryptOfMd5Utils.md5DigestAsHex(rawPassword.toString());
return encryptText.equals(encodedPassword);
}
}
- 用户信息查询获取的提供者
/**
* @author: liukj
* @date: 2019/12/17
* @description: 用户信息详情查询服务
*/
@Component("userDetailServiceImpl")
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
IUserInfoSvc iUserInfoSvc;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if(StringUtils.isBlank(username)){
return new UserDetailInfo();
}
UserAuthority userAuthority = iUserInfoSvc.queryUserAuthority(null,username);
if(null == userAuthority || StringUtils.isBlank(userAuthority.getUserId())){
throw new MyException(ExcepEnum.ERROR_12_01.getExcepCode(),ExcepEnum.ERROR_12_01.getExcepDes(username));
}
if(StatusEnums.INVALID.status.equals(userAuthority.getUserInfo().getUserStatus())){
throw new MyException(ExcepEnum.ERROR_12_01.getExcepCode(),ExcepEnum.ERROR_12_01.getExcepDes(username));
}
List<RoleInfo> roleInfoList = userAuthority.getRoleInfoList();
UserDetailInfo userDetails = new UserDetailInfo();
//设置用户基本信息
userDetails.setUserAuthority(userAuthority);
Collection<GrantedAuthority> authoritiesList = new ArrayList<>();
for(RoleInfo roleInfo : roleInfoList){
GrantedAuthority simpleAuthority = new SimpleGrantedAuthority(roleInfo.getRoleCode());
authoritiesList.add(simpleAuthority);
}
//设置用户角色信息
userDetails.setAuthorities(authoritiesList);
return userDetails;
}
}
- 密码更新提供者
/**
* @author: liukj
* @date: 2019/12/17
* @description: 密码更新
*/
@Component("userDetailsPasswordServiceImpl")
public class UserDetailsPasswordServiceImpl implements UserDetailsPasswordService {
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
return null;
}
}
- 配置文件
关于配置,主要参考第二点中配置详解,然后这里投票表决采用的都是默认的,只需在要访问的资源后面加上.hasRole(role);即可控制住某些角色可访问那些资源。
/**
* @author: liukj
* @date: 2019/9/22
* @description: springSecurity配置
*/
@Configuration
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
private static final Logger logger = LoggerFactory.getLogger(MySecurityConfig.class);
/**
* 资源权限加载服务
*/
@Autowired
private LoadResourceSvc loadResourceSvc ;
/**
* 自定义注销成功处理器
*/
@Autowired
private UserLogoutSuccessHandler userLogoutSuccessHandler;
/**
* 自定义暂无权限处理器
*/
@Autowired
private UserAuthAccessDeniedHandler userAuthAccessDeniedHandler;
/**
* 自定义未登录的处理器(认证失败的处理点)
*/
@Autowired
private UserAuthenticationEntryPointHandler userAuthenticationEntryPointHandler;
@Autowired
private MyUsernamePwdAuthenticationFilter myUsernamePwdAuthenticationFilter;
private String[] append(List<String> resources){
StringBuffer resorceStr = new StringBuffer();
for (int i = 0; i < resources.size(); i++) {
String resource = resources.get(i);
resorceStr.append(resource).append(",");
}
if(StringUtils.isNotBlank(resorceStr)){
return resorceStr.substring(0,resorceStr.length()-1).split(",");
}
return null;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
List<String> roleModulePermitList = loadResourceSvc.getRoleModulePermitList();
// 添加无需授权的请求配置
String[] permitList = append(roleModulePermitList);
if(null != permitList){
http.authorizeRequests().antMatchers(permitList).permitAll();
}
//添加认证的请求列表
Map<String, List<String>> roleModuleAuthMap = loadResourceSvc.getRoleModuleAuthMap();
for(Map.Entry entry : roleModuleAuthMap.entrySet()){
String role = (String) entry.getKey();
String[] authList = append(roleModuleAuthMap.get(role));
if(null != authList){
http.authorizeRequests().antMatchers(authList).hasRole(role);
}
}
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
//异常处理配置
.exceptionHandling()
//授权失败的处理逻辑
.accessDeniedHandler(userAuthAccessDeniedHandler)
//自定义认证失败的处理逻辑
.authenticationEntryPoint(userAuthenticationEntryPointHandler)
.and()
// 开启跨域
.cors()
.and()
// 取消跨站请求伪造防护
.csrf().disable()
//session管理配置
.sessionManagement()
//基于Token不需要session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.headers()
//禁用缓存
.cacheControl().disable()
.and()
//添加自定义过滤器
.addFilterBefore(myUsernamePwdAuthenticationFilter,JWTAuthenticationTokenFilter.class)
.addFilter(new JWTAuthenticationTokenFilter(authenticationManager()));
}
}
自己在学习整理这个太不容易了,希望大家在引用的时候标注一下。多谢了,共同学习,共同进步!