引言

一般情况下 , Spring Secuirty默认的配置是不够,常常需要自定义化.本文就Spring Security对接

  1. 用户名密码登录(普通认证) NORMAL_AUTHENTICATION
  2. 不需要用户名密码登录(预览认证) PREVIEW_AUTHENTICATION
  3. CAS登录认证 CAS_AUTHENTICATION
  4. PKI认证 PKI_AUTHENTICATION
  5. 混合认证 e.g. PKI_AND_NORMAL_AUTHENTICATION
  6. JL-CAS对接技巧 JL_CAS_AUTHENTICATION
  7. more and more ...
    展开讨论

and more

Spring Security CookieSession和HeaderSession ->HttpSessionIdResolver
Spring Security Concurrency Controller
Spring Security + Spring Session ...

核心思路

使用 @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配置代码放到文章末尾,防止太长,引起大家反感.

  1. 引入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>
  1. 进行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

  1. 直接使用JL-CAS中已经写好的Filter和当前Spring Secuirty Filter链进行整合处理,提取相关配置到Config中
  2. 使用原生的CAS系统,Security-CAS模块,进行简单整改即可使用。

Other Config

csrf

当前端请求接口失败时可以关闭,正常来说是不应该关闭的.

http.csrf().disable();

一般来说csrf防御措施:

  1. 验证 HTTP Referer 字段
  2. 添加 token 并验证
  3. 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中的异常的,为了能够统一异常处理.

  1. 定义一个ExceptionFilter 并放置在Filter链的最前端
  2. 捕获chain.doFilter(request, response);的异常,存储到Attribute中
  3. foward到指定controller中 假定地址 -> /error/rethrow
  4. /error/rethrow 中重新抛出异常
  5. 在@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

正常做法是

  1. 新增AuthenticationProvider (可以顺便新增自己的Authentication其Details存储认证信息)
  2. 新增AuthenticationFilter 它继承自 AbstractAuthenticationProcessingFilter
  3. 把他们添加到Spring Security的链路中

core 每一类认证系统就是authentication+AuthenticationFilter+AuthenticationProvider

其中AuthenticationFilter作用是管理请求来临是你要哪些信息,认证成功后你要做什么。