赞
踩
SpringSecurity是一种用于身份验证和访问控制的强大且高度可定制化的框架,它是Spring应用程序中,要实现程序级别安全性的主要选择,SpringSecurity提供了一种高度定制的身份验证,授权,防止常见攻击(固定会话攻击,XSS脚本攻击,CSRF攻击等)的方法。
如果你现在正在使用Spring框架进行Web开发,并且有程序安全性的需求,那么使用SpringSecurity将是一个不错的选择。
《SpringSecurity in Action》那本书中提到,学习SpringSecurity确实有不小的成本。其中一个成本就是它很容易给学习者带来一些困扰,因为它是可高度定制的,所以我们在不同的网站,或是不同的书籍中,会看到对同一功能有不同配置方式,以致于我们不知从何选择。其实只要知道其中一种就OKAY了。还有一个学习成本就是SpringSecurity里的概念实在是太多了,实在是太多了。比如初学时接触的Authentication
这个对象,就很够初学者喝几壶的了。不过没关系,只要我们抱着不去精通SpringSecurity的心态,就会慢慢地放下焦虑,日拱一卒式的掌握SpringSecurity。
本文是基于Session实现登陆认证,所以有必要先说一下这个基于Session的原理。SpringSecurity的核心功能,就是通过一系列的过滤器来实现安全性的。其中有一个过滤器叫SecurityContextPersistenceFilter
,在这个过滤器里,有两行关键代码SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
与repo.saveContext(contextAfterChainExecution, holder.getRequest(),holder.getResponse());
。repo
是一个SecurityContextRepository
对象,其中一个实现类就是HttpSessionSecurityContextRepository
。看名子就很容易理解,这是一个基于HttpSession的SecurityContext的仓库(一不小心就又出现了一个SpringSecurity时的另一个概念–SecurityContext)。其实过程就是登陆请求中,把用户的登陆成功后的信息保存在HttpSession中,在其它请求中,从HttpSession中获取登陆成功后的信息。HttpSessionSecurityContextRepository
就是存储这些HttpSession的地方,类似于是一个数据库。
基于Session实现登陆认证,是SpringSecurity默认的登陆认证方式,不需要我们做额外的配置。
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.20</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
代码中使用到了一些工具类,如CommonResult
,该工具类并没有在代码中,这里只针对SpringSecurity的核心内容进行了说明。如果需要完成的代码示例,可以到gitee上下面完成代码 hgd11-security security_session分支。
package cn.hgd11.security.config;
import cn.hgd11.security.common.CommonResult;
import cn.hgd11.security.enums.GlobalCodeEnum;
import cn.hgd11.security.util.StringUtils;
import cn.hutool.extra.servlet.ServletUtil;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* @author wangkaige
*/
@EnableWebSecurity(debug = true)
public class Hgd11SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/*
* 在这里配置基于内存的用户,并且指定了不使用加密的PasswordEncoder,学习阶段完全没有这个必要了。
*/
InMemoryUserDetailsManager inMemoryUserDetails = new InMemoryUserDetailsManager();
// 向内存中注册两个用户
inMemoryUserDetails.createUser(User.withUsername("testUser").password("123").authorities("read").build());
inMemoryUserDetails.createUser(User.withUsername("testUser02").password("123").authorities("read").build());
auth.userDetailsService(inMemoryUserDetails)
.passwordEncoder(passwordEncoder());
// 通过匿名实现类定义一个验证码登陆的Provider
DaoAuthenticationProvider varificationCodeProvider = new DaoAuthenticationProvider() {
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
/**
* 在这里想模拟一下验证码登陆方式
* {@link ProviderManager#authenticate(Authentication)}类里说的好,【If more than one
* AuthenticationProvider supports the passed Authentication object, the first
* one able to successfully authenticate the Authentication object determines the result,】
* 当有多个 AuthenticationProvider 时,第一个成功认证的 AuthenticationProvider 将决定最后的结果。用中国话就是,谁先认证成功了,谁说了算,认证过程到此结束。
* 所以我们在这里加一个 AuthenticationProvider 用于验证码登陆的认证。如果使用验证码的话, DaoAuthenticationProvider 认证肯定是失败的,所以会走到这里来。
*
* DaoAuthenticationProvider 的 additionalAuthenticationChecks 方法做了两个事情 一是校验了密码是否为null,二是校验了密码是否正确
* 这里想模拟的是验证码登陆,所以照猫画虎,也做两步校验就OKAY了,一是校验验证码是否为null,二是校验难证码是否正确。
*/
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
/*
* authentication 里的 credentials 是在 UsernamePasswordAuthenticationFilter里获取的
* String username = obtainUsername(request);
* String password = obtainPassword(request);
* 所以这里要求前端在用验证码请求时,那个验证码的参数名也得是'password',当前username的参数值应该是一个手机号什么的。
* 如果想自定义验证码的参数名,那就不能使用这种方案了。
* 不过想一下,没必要为了被一些人吹捧的什么程序员强迫症,就非要自定义参数,用这种方案最贴近SpringSecurity,我们为什么不采用呢。
*/
String verificationCode = authentication.getCredentials().toString();
/*
* 下一步就是验证 verificationCode 的正确性,这里大多都是在获取验证码请求中,把验证码存到了redis中,这里就假装从redis里获取一下,然后校验成功就行了。
*/
// 从redis获取当前用户的验证码
// 假装认证成功,然后什么也不做就行了,如果认证失败,这里是用抛出异常的形式体现的,什么也不发生表示没有坏事儿发生。
}
};
varificationCodeProvider.setUserDetailsService(inMemoryUserDetails);
auth.authenticationProvider(varificationCodeProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
/*
* 对于SpringSecurity来说,在不同的示例中,对同一功能可能会看到各种各样的配置。
* 这是因为SpringSecurity对于同一个功能,给出了非常灵活的配置方式。但这种灵活性却给我们带来了其些困扰,让我们觉得学习起来非常的凌乱。
* 其实这并不应该归罪于SpringSecurity,我们只要在学习过程中掌握其中的一种即可。
*/
http
/*
* 先禁用csrf防护
*/
.csrf().disable()
/*
* form表单方式登陆
* 该配置会开放一个 form-data 的端点(SpringSecurity里叫EntryPoint,翻译成端点),用于登陆请求,用户名参数为-username 密码参数为-password
* 在非前后端分离的场景中,通常还会配置一个 loginPage() 和 loginProcessingUrl()
* 在这个场景下就不需要了,除非我们想自定义登陆端点的地址,可以配置为 loginProcessingUrl("/custom/login")
* loginPage() - 用来获取登陆页面,就是那个填写用户名密码的页面,在前后端分离的场景中当然就不需要这个配置
* loginProcessingUrl() - 用来配置登陆请求,就是所谓的真正的那个登陆请求,为POST请求。就是填写完用户名密码后,要把用户名密码提交到的端口
*
* 相对于formLogin,SpringSecurity还有一个 httpBasic()的登陆方式,也是SpringSecurity默认的登陆方式。它与 formLogin的不同在于。。。 。。。
* httpBasic其它算不上是一个要登陆认证后才能去请示资料的一种方式,httpBasic是要求在第个请示中,都传入用户名与密码,在每个请示中都检验用户名与密码,
* 也就是说,这种方式并不要求一定要有个登陆的环境,只要在请示资源时,如请示用户列表这个接口,在请示中加入用户名与密码就可以进行请示操作了。
* 这样很明显不太安全,在每个请示中都传送用户名与密码的方式,密码漏掉的可能性就会变大。
* httpBasic请示的示例像这样: curl -X GET -u user:password http://localhost:8080/user/list
* 或者是 curl -X GET -H 'Authorization: {user:password的base64编码}' http://localhost:8080/user/list
*/
.formLogin()
/*
* 登陆成功后的处理器
* 当登录成功后,会进入到这里进行处理后续的事宜
* 在前后端分离的场景中,这里通常会返回一个告知登陆成功的ajax响应,也可以带上token返回。到底带不带token视实际场景而定。
* SpringSecurity还提供了一个 successForwardUrl() 配置,参数接口一个url,用于配置登陆成功后要跳转的url地址
* 在前后端分离的场景,登陆成功后要跳转到哪里去,应该由前端页面自己决定。后端只需要告知登陆是成功还是失败即可。
*/
.successHandler((request, response, authentication) -> ServletUtil.write(response, JSONObject.toJSONString(CommonResult.success("登陆成功")), MediaType.APPLICATION_JSON_UTF8_VALUE))
/*
* 登录失败的处理器,同successHandler,这里返回一个告知登陆失败的ajax响应
* 不同的是,这个处理器的入参里,不是一个Authentication对象,而是一个AuthenticationException,我们可以根据不同的异常类型,返回不同的响应信息
* 类似于
* if (exception instanceof PasswordException) {
* // 返回密码错误
* // 但是不建议明确地告知是密码错误,因为这样用户让骇客们知道,使用的用户名是正常的,只是密码有误而已。
* // 所以我们通常都会返回【用户名或密码错误】
* } else {
* // 其它登陆异常信息
* }
*
*/
.failureHandler((request, response, exception) -> ServletUtil.write(response, JSONObject.toJSONString(CommonResult.error(GlobalCodeEnum.UNAUTHORIZED.getCode(), "登陆异常")), MediaType.APPLICATION_JSON_UTF8_VALUE))
.and()
.logout()
.logoutSuccessHandler((request, response, authentication) -> ServletUtil.write(response, JSONObject.toJSONString(CommonResult.error(GlobalCodeEnum.GL_SUCC_200.getCode(), "登出成功")), MediaType.APPLICATION_JSON_UTF8_VALUE))
.and()
/*
* 认证失败的处理器,区别与 failureHandler
* failureHandler - 在登陆请求中,由于密码错误,或是用户名错误等一些异常,造成的登陆失败时的处理器
* exceptionHandling - 是在正常请求中,比如用户请求 /user/list 接口,发现token超时了,或是其它的认证失败的情况,会进入到这里进行处理
*
* 同样都是认证失败的处理方式,为什么要有两个配置呢?其它这个问题应该说为什么不能有两个配置呢?这里最终都是通过 AuthenticationEntryPoint 的实现类进行处理的,我们完全可以
* 用同一个实现类配置在 failureHandler 与 exceptionHandling 里面,但 SpringSecurity 给了我们可以分别配置的可能,可以认为这是一种灵活性的体现。
*
* 也可以在 exceptionHandling 中传入一个入参。配置 authenticationEntryPoint 认证失败的 authenticationEntryPoint,效果是一样的
* exceptionHandling(
* httpSecurityExceptionHandlingConfigurer ->
* httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(
* (request, response, authException) ->
* ServletUtil.write(response, JSONObject.toJSONString(CommonResult.error(GlobalCodeEnum.UNAUTHORIZED.getCode(), "登陆超时或未登陆")), MediaType.APPLICATION_JSON_UTF8_VALUE))
* )
*/
.exceptionHandling()
.authenticationEntryPoint((request, response, authException) -> ServletUtil.write(response, JSONObject.toJSONString(CommonResult.error(GlobalCodeEnum.UNAUTHORIZED.getCode(), "登陆超时或未登陆")), MediaType.APPLICATION_JSON_UTF8_VALUE))
.and()
/*
* 配置认证规则
* 这个是一层一层进行配置的,通过 .and() 进行连接
* 最先配置的优先级最高,比如下面的配置,在最后的时候配置了所有的请求都需要进行认证才能访问
* 前面配置了 test/anonymous 可以匿名访问,那么 hgd11-security-api/test/anonymous可以匿名访问
* 这条规则会生效
*
* 这里需要注意的是,如果在配置文件中配置了 server.servlet.context-path ,这里配置路径规则时,不需要加 server.servlet.context-path 里
* 配置的请求前缀,SpringSecurity在进行路径匹配时,对比的是uri。可能在SpringMVC里,uri是指去掉server.servlet.context-path的url,这里就不是
* 很清楚了。
*
* 可以匿名访问的路径与permitAll的请示路径,在结果上有相同的效果,到底SpringSecurity对此有什么高级的应用,这个还不得而知。如果只是想放行一些请示路径
* 的话,这里使用哪种方式其它都是可以的了。
*/
// 1 认证规则配置
.authorizeRequests()
// 2 表示 swagger-ui.html这个请求
.mvcMatchers("test/swagger-ui.html")
// 3 允许所有任何用户以任何方式进行请求
.permitAll()
.and()
// 1 认证规则配置
.authorizeRequests()
// 2 表示 test/anonymous 这个请求
.mvcMatchers("test/anonymous")
// 3 允许匿名请问
.anonymous()
.and()
// 1 认证规则配置
.authorizeRequests()
// 2 表示所有请求
.anyRequest()
// 3 都需要认证才能请求
.authenticated();
}
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception
用于配置AuthenticationManager
这是在认证过程中一个非常重要的组件,整个认证过程,如获取正在登陆的用户信息,验证用户密码的操作都是在AuthenticationManager
里完成的。AuthenticationManager
一个最常见的实现类是ProviderManager
,我们可以实现自己的AuthenticationProvider
,然后注册到ProviderManager
里,从而实现自定义的认证逻辑。
本代码示例中使用的是基于内存的UserDetailsService
–InMemoryUserDetailsManager
,测试学习当然是可以使用这个实现类的。
本代码示例中还有一个匿名的AuthenticationProvider
实现类,这个实现类是从DaoAuthenticationProvider
继承过去的。这个实现类模拟了如何基于手机验证码进行登陆认证。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。