引言
一般情况下 , Spring Secuirty默认的配置是不够,常常需要自定义化.本文就Spring Security对接
- 用户名密码登录(普通认证) NORMAL_AUTHENTICATION
- 不需要用户名密码登录(预览认证) PREVIEW_AUTHENTICATION
- CAS登录认证 CAS_AUTHENTICATION
- PKI认证 PKI_AUTHENTICATION
- 混合认证 e.g. PKI_AND_NORMAL_AUTHENTICATION
- JL-CAS对接技巧 JL_CAS_AUTHENTICATION
- more and more ...
展开讨论
and more
Spring Security CookieSession和HeaderSession ->HttpSessionIdResolver
Spring Security Concurrency Controller
Spring Security + Spring Session ...
江南一点雨 - Spring Secuirty 关于这块讲的不错
核心思路
使用 @EnableWebSecurity 开启WebSecuirty
通过继承WebSecurityConfigurerAdapter来重写相关方法来自定义化.
用户名密码登录 NORMAL_AUTHENTICATION
一般需要配置以下内容:
- PasswordEncoder
- UserDetailsService
- AuthenticationEntryPoint
- AuthenticationSuccessHandler
- AuthenticationFailureHandler
- LogoutSuccessHandler
[cnblog-> spring security架构] (https://www.cnblogs.com/yanzhenjingyan/p/10382594.html)
预览认证 PREVIEW_AUTHENTICATION
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().permitAll().and().logout().permitAll();
http.addFilterBefore(previewAuthenticationFilter(), WebAsyncManagerIntegrationFilter.class);
}
因为Spring Secuirty核心是验证SecurityContextHolder.getContext().getAuthentication();
所以我们构造一个可登录的用户在Filter中即可
CAS登录认证 CAS_AUTHENTICATION
CAS配置代码放到文章末尾,防止太长,引起大家反感.
- 引入spring-secuirty和cas相关包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.6.1</version>
</dependency>
<!-- Spring Security For CAS -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
<version>4.1.0.RELEASE</version>
</dependency>
- 进行CAS相关配置
详情见文末CAS相关代码
PKI认证 PKI_AUTHENTICATION
纯PKI认证其实可预览认证有些相似,使用一个Filter进行处理,使之与Spring Secuirty结合即可
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().disable();
http.authorizeRequests()
.anyRequest().permitAll().and().logout().permitAll();
http.addFilterAfter(pkiAuthenticationFilter(), AnonymousAuthenticationFilter.class);
}
在使用bean配置Filter的时候,Filter会被创建两次,即会走两次可以通过以下方式处理
private static final String FILTER_APPLIED = "__spring_security_pkiAuthenticationFilter_filterApplied";
if (servletRequest.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(servletRequest, servletResponse);
return;
}
混合认证 e.g. PKI_AND_NORMAL_AUTHENTICATION
混合认证的时候需要注意的是PKI和NORMAL中的鉴权和放行要一致,
PKI中要放行登录登出等操作.PKI的Filter要允许用户未登录的情况.
if (excludeAuthentication.contains(request.getRequestURI())) {
chain.doFilter(request, response);
return;
}
JL-CAS对接技巧 JL_CAS_AUTHENTICATION
- 直接使用JL-CAS中已经写好的Filter和当前Spring Secuirty Filter链进行整合处理,提取相关配置到Config中
- 使用原生的CAS系统,Security-CAS模块,进行简单整改即可使用。
Other Config
csrf
当前端请求接口失败时可以关闭,正常来说是不应该关闭的.
http.csrf().disable();
一般来说csrf防御措施:
- 验证 HTTP Referer 字段
- 添加 token 并验证
- HTTP 头中自定义属性并验证
Spring Secuirty是添加_csrf
字段进行防御的.
前端适配方式 : 可以在ajax或form提交之前通过接口获取csrf_token,并追加到header或parameter中,然后一并提交
SuccessHandler
实现 AuthenticationSuccessHandler接口
重写onAuthenticationSuccess方法,目的是用户登录成功后进行一些处理.
e.g. 更新用户最后一次登录时间
e.g. 记录登录日志等.
e.g. 登录成功返回特定json信息
FailureHandler
实现 AuthenticationFailureHandler接口
重写 onAuthenticationFailure方法,目的是用户登录失败后进行一些处理
e.g. 根据异常 exception.getCause()
返回不同登录失败原因
e.g. 登录失败返回特定json信息
MacLoginUrlAuthenticationEntryPoint
实现 AuthenticationEntryPoint 接口
当用户没有携带有效信息访问 -> 需鉴权接口时 会走到这里
默认的实现方式是403 || 错误页
如果需要特殊处理,如HttpStatus为200,返回的json体中进行错误码响应时,重写此方法即可.
ExceptionFilter
在SpringMVC中@ControllerAdvice
是无法捕获Filter中的异常的,为了能够统一异常处理.
- 定义一个ExceptionFilter 并放置在Filter链的最前端
- 捕获
chain.doFilter(request, response);
的异常,存储到Attribute中 - foward到指定controller中 假定地址 ->
/error/rethrow
- 在
/error/rethrow
中重新抛出异常 - 在@ControllerAdive中进行处理
以上就是统一处理Filter中的异常.避免传统使用 response.getWriter()的方式.简单优雅.
GlobalDefaultExceptionHandler
在项目中我们经常会定义一个全局异常处理来统一返回的数据结构 关键注解@RestController
下面列出常处理的异常
- MethodArgumentNotValidException || 使用@Valid校验失败
- MissingServletRequestParameterException || @RequestParam 参数未找到
- MethodArgumentTypeMismatchException || 在@RequestMapping中注入Object 但是不存在
- BindException || 在 TypeConverter失败时
- HttpRequestMethodNotSupportedException || 请求方式未找到 如用GET方式调用POST接口
- HttpMessageNotReadableException || @RequestBody 接受不到时 前端应使用json传输
- ConstraintViolationException || 在方法上参数使用 @NotBlank等校验失败时
- Exception || 兜底异常
- CustomerException || 自定义RuntimeException
下面是一个异常获取核心信息的方法,分享出来
private Map<String, String> from2Message(List<ObjectError> allErrors) {
Map<String, String> errorMap = new HashMap<>(allErrors.size());
for (ObjectError allError : allErrors) {
if (Objects.requireNonNull(allError.getDefaultMessage()).length() > 100) {
errorMap.put(((FieldError) allError).getField(), ((FieldError) allError).getRejectedValue() + "格式不正确");
continue;
}
if (allError instanceof FieldError) {
errorMap.put(((FieldError) allError).getField(), allError.getDefaultMessage());
} else {
errorMap.put(allError.getObjectName(), allError.getDefaultMessage());
}
}
return errorMap;
}
写在文章末尾
Spring Secuirty只是Spring Boot帮我们封装好的一种工具,以上所有功能都可以通过Java Web方式实现,只不过实现的难度大小,实现的兼容情况不尽相同.
最终想说的是,当你想用Spring Security整合某种登录时,先想清楚用filter如何整合.然后结合起来,so easy.
最终Spring Secuirty能帮我们的是,当登录方式切换时,代码改动量很少. 也很少有项目需要不断改登录方式,GA除外...
Append
CAS 配置代码
其中CASProperties配置
/**
* @author wangqimeng
* @date 2019/12/5 11:09
*/
@Data
public class CasConfigProperties {
/**
* CAS服务登录地址
*/
private String casServerUrl;
/**
* CAS服务登录地址
*/
private String casServerLoginUrl;
/**
* CAS服务登出地址
*/
private String casServerLogoutUrl;
/**
* app地址
*/
private String appServerUrl;
/**
* app 登录地址
*/
private String appLoginUrl;
/**
* app登出地址
*/
private String appLogoutUrl;
}
其中 WebSecurityConfigurerAdapter 配置
@EnableWebSecurity
@ConditionalOnProperty(prefix = "authentication", name = "type", havingValue = "cas_authentication")
public static class CasAuthenticationConfiguration extends WebSecurityConfigurerAdapter {
@Resource
private CasConfigProperties casProperties;
@Resource
private AuthenticationConfigProperties authenticationConfigProperties;
@Bean
@ConfigurationProperties(prefix = "sso")
public CertificateProperties certificateProperties() {
return new CertificateProperties();
}
@Bean
@ConfigurationProperties(prefix = "authentication.cas-config")
public CasConfigProperties casProperties() {
return new CasConfigProperties();
}
@Bean
@ConditionalOnProperty(prefix = "sso", value = "auto-import-certificate", havingValue = "true", matchIfMissing = false)
public AutoImportRunner autoImportRunner() {
return new AutoImportRunner();
}
/**
* 定义认证用户信息获取来源,密码校验规则等
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
auth.authenticationProvider(casAuthenticationProvider());
}
@Override
public void configure(WebSecurity web) throws Exception {
web.debug(authenticationConfigProperties.isDebug());
}
/**
* 定义安全策略
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().and()
.headers()
.frameOptions().sameOrigin()
.xssProtection()
.block(true);
http
.headers()
.cacheControl()
.and()
.contentTypeOptions()
.and()
.httpStrictTransportSecurity()
.and()
.xssProtection();
http.authorizeRequests()
//配置安全策略
.antMatchers("/api/**").authenticated()//login下请求需要验证
.and()
.logout()
.permitAll()
//定义logout不需要验证
.and()
//使用form表单登录
.formLogin();
http.exceptionHandling().authenticationEntryPoint(customerCasAuthenticationEntryPoint())
.and()
.addFilter(casAuthenticationFilter(casAuthenticationSuccessHandler()))
.addFilterBefore(casLogoutFilter(), LogoutFilter.class)
.addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class)
;
//禁用CSRF
http.csrf().disable();
}
@Bean
public CasAuthenticationSuccessHandler casAuthenticationSuccessHandler() {
return new CasAuthenticationSuccessHandler();
}
@Bean
public ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> singleSignOutHttpSessionListener() {
ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> servletListenerRegistrationBean = new ServletListenerRegistrationBean<>();
servletListenerRegistrationBean.setListener(new SingleSignOutHttpSessionListener());
servletListenerRegistrationBean.setEnabled(true);
return servletListenerRegistrationBean;
}
/**
* 指定service相关信息
*/
@Bean
public ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService(casProperties.getAppServerUrl() + casProperties.getAppLoginUrl());
serviceProperties.setAuthenticateAllArtifacts(true);
return serviceProperties;
}
/**
* 认证的入口
*/
@Bean
public CustomerCasAuthenticationEntryPoint customerCasAuthenticationEntryPoint() {
CustomerCasAuthenticationEntryPoint customerCasAuthenticationEntryPoint = new CustomerCasAuthenticationEntryPoint();
customerCasAuthenticationEntryPoint.setLoginUrl(casProperties.getCasServerLoginUrl());
customerCasAuthenticationEntryPoint.setServiceProperties(serviceProperties());
customerCasAuthenticationEntryPoint.setAuthenticationRedirectStrategy(new CasAuthenticationRedirectStrategy());
return customerCasAuthenticationEntryPoint;
}
/**
* CAS认证过滤器
*/
@Bean
public CasAuthenticationFilter casAuthenticationFilter(CasAuthenticationSuccessHandler casAuthenticationSuccessHandler) throws Exception {
CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
casAuthenticationFilter.setAuthenticationManager(authenticationManager());
casAuthenticationFilter.setFilterProcessesUrl(casProperties.getAppLoginUrl());
casAuthenticationFilter.setAuthenticationSuccessHandler(casAuthenticationSuccessHandler);
return casAuthenticationFilter;
}
@Bean
public Cas30ServiceTicketValidator cas30ServiceTicketValidator() {
return new Cas30ServiceTicketValidator(casProperties.getCasServerUrl());
}
/**
* cas 认证 Provider
*/
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
casAuthenticationProvider.setAuthenticationUserDetailsService(customUserDetailsService());
//casAuthenticationProvider.setUserDetailsService(customUserDetailsService()); //这里只是接口类型,实现的接口不一样,都可以的。
casAuthenticationProvider.setServiceProperties(serviceProperties());
casAuthenticationProvider.setTicketValidator(cas30ServiceTicketValidator());
casAuthenticationProvider.setKey("casAuthenticationProviderKey");
return casAuthenticationProvider;
}
/**
* 用户自定义的AuthenticationUserDetailsService
*/
@Bean
public AuthenticationUserDetailsService<CasAssertionAuthenticationToken> customUserDetailsService() {
return new CasUserDetailsService();
}
/**
* 单点登出过滤器
*/
@Bean
public SingleSignOutFilter singleSignOutFilter() {
SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
singleSignOutFilter.setIgnoreInitConfiguration(true);
return singleSignOutFilter;
}
/**
* 请求单点退出过滤器
*/
@Bean
public LogoutFilter casLogoutFilter() {
LogoutFilter logoutFilter = new LogoutFilter(casProperties.getCasServerLogoutUrl(), new SecurityContextLogoutHandler());
logoutFilter.setFilterProcessesUrl(casProperties.getAppLogoutUrl());
return logoutFilter;
}
}
其中CasUserDetailsService,需要的字段需要自己自定义
/**
* 用于加载用户信息 实现UserDetailsService接口,或者实现AuthenticationUserDetailsService接口
*/
public class CasUserDetailsService implements AuthenticationUserDetailsService<CasAssertionAuthenticationToken> {
@Override
public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) throws UsernameNotFoundException {
// 结合具体的逻辑去实现用户认证,并返回继承UserDetails的用户对象;
System.out.println("当前的用户名是:"+token.getName());
//获取用户信息
UserInfo userInfo = new UserInfo();
userInfo.setUsername(token.getName());
userInfo.setRole("ROLE_USER");
Map<String, Object> userAttributess = token.getAssertion().getPrincipal().getAttributes();
//System.out.println(userAttributess.toString());
if (userAttributess != null) {
userInfo.setId( String.valueOf(userAttributess.get("id")));
userInfo.setNickname( String.valueOf(userAttributess.get("nickname")));
userInfo.setRealName( String.valueOf(userAttributess.get("real_name")));
userInfo.setEmail( String.valueOf(userAttributess.get("email")));
userInfo.setCountryCode( String.valueOf(userAttributess.get("country_code")));
}
System.out.println(userInfo.toString());
return userInfo;
}
}
关于项目启动导入CAS证书的工具类
link my csdn blog -> JVM证书导入: 通过java代码导入证书
isAssignableForm 和instanceof区别
isAssignableFrom()方法与instanceof关键字的区别总结为以下两个点:
isAssignableFrom()方法是从类继承的角度去判断,instanceof关键字是从实例继承的角度去判断。
isAssignableFrom()方法是判断是否为某个类的父类,instanceof关键字是判断是否某个类的子类。
Append
正常做法是
- 新增AuthenticationProvider (可以顺便新增自己的Authentication其Details存储认证信息)
- 新增AuthenticationFilter 它继承自 AbstractAuthenticationProcessingFilter
- 把他们添加到Spring Security的链路中
core 每一类认证系统就是authentication+AuthenticationFilter+AuthenticationProvider
其中AuthenticationFilter作用是管理请求来临是你要哪些信息,认证成功后你要做什么。