项目spring boot实践系列2-spring security

一、Spring security 概述

1、简介

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于Spring应用程序的事实标准。

Spring Security是一个专注于为Java应用程序提供身份验证和授权的框架。与所有Spring项目一样,Spring Security的真正威力在于它可以容易地扩展以满足自定义需求。

Spring Security作为安全框架,它不可避免和用户、资源打交道,作为用户权限认证这块,我们一般用的RBAC,即基于角色的权限认证,本节不展开说明,重点关注Spring Security。

另外Spring Security是OAuth2的一种实现,有兴趣的同学可以参考阮一峰的OAuth2的文章,理解OAuth2对授权认证的理解非常有帮助。

2、两大安全框架:spring security和shiro

目前java 领域有两大安全框架,除了spring security,还有shiro,后面一节会专门说明,两者基本功能一致,主要有如下不同点:

1) Spring security基于Spring,是Spring 家族的一部分,而shiro作为独立框架,可Shiro可依赖Spring运行;

2) Spring security的功能更多,比如安全防护部分;

3) Shiro的上手更简单;

目前一般项目基本基于Spring boot,与Spring security配合更好,但老的项目,可能由于上手简单等因素,会选择shiro,所以维护shiro的项目会多些。

2、主要功能

1)认证,认证解决的“是谁”的问题;

2)授权,授权解决的是“能做啥”的问题;

3)安全防护,比如CSRF,Response Headers等等,这里不是重点。

二、Spring Security原理

1、Spring Security 架构

Spring Security的Servlet支持是基于Servlet filers的,因此首先了解过滤器的作用是很有帮助的。客户端向应用程序发送一个请求,容器创建一个FilterChain,其中包含Filter实例和Servlet,它们应该根据请求URI的路径来处理HttpServlet请求。在Spring MVC应用程序中,Servlet是DispatcherServlet的一个实例。一个Servlet最多可以处理一个HttpServlet请求和HttpServlet响应。但是,可以使用多个筛选器。

Spring提供了一个名为DelegatingFilterProxy的Filter实现,允许在Servlet容器的生命周期和Spring的ApplicationContext之间建立桥梁。Servlet容器允许通过使用自己的标准来注册Filter实例,但它不知道Spring定义的Beans。你可以通过标准的Servlet容器机制来注册DelegatingFilterProxy,将所有工作委托给实现Filter的Spring Bean。

SpringSecurity的Servlet支持包含在FilterChainProxy中。FilterChainProxy是Spring Security提供的一种特殊筛选器,允许通过SecurityFilterChain将其委托给许多筛选器实例。由于FilterChainProxy是一个Bean,它通常封装在DelegatingFilterProxy中。

在“多重安全过滤器链”图中,FilterChainProxy决定应使用哪个安全过滤器链。只调用第一个匹配的SecurityFilterChain。如果请求/api/messages/的URL,它首先在/api/**的SecurityFilterChain0模式上匹配,因此只调用SecurityFilterChain 0,即使它在SecurityFilterChainn上也匹配。如果请求了/messages/的URL,则该URL在/api/**的SecurityFilterChain0模式上不匹配,因此FilterChainProxy将继续尝试每个SecurityFilterChain。假设没有其他SecurityFilterChain实例匹配,则调用SecurityFilterChainn。

2、Spring Security Authentication 架构

在认证环节,Spring security使用Authentication对象,封装了对安全主体的表示。Authentication对象将作为SecurityContext中的重要组成部分,而SecurityContext正是作为Spring security为应用提供安全管理控制的上下文基础。应用怎么使用SecurityContext呢?Spring security提供了SecurityContextHolder来维护SecurityContext对象,以便应用中可以在一次用户级会话处理过程中随时可以使用安全上下文。

Spring security将会使用UserDetails来构造Authenticaiton,而应用也往往会将UserDetails转型为业务领域中的对应对象。在授权环节的基础是SecurityContextHolder的Authenticasiton除了包含主体信息外,还会包含其关联的权限信息。这些权限信息通常也都是通过UserDetailsService加载而来的。使用authentication.getAuthorities()得到主体现有权限,匹配安全对象所需的访问权限,若满足则给予授权放行,反之与之拒绝。

相关术语含义:

SecurityContextHolder - SecurityContextHolder是Spring Security存储认证的详细信息的地方。

SecurityContext - 从SecurityContextHolder中获得,包含当前认证用户的认证。

Authentication - 可以是AuthenticationManager的输入,以提供用户提供的认证凭证或来自SecurityContext的当前用户。

GrantedAuthority - 在认证中授予委托人的权限(即角色、作用域等)。

AuthenticationManager - 定义Spring Security的过滤器如何执行认证的API。

ProviderManager--AuthenticationManager的最常见的实现。

AuthenticationProvider - 由ProviderManager用于执行特定类型的认证。

3、几个重要的Filter

1)UsernamePasswordAuthenticationFilter:提供认证机制,服务于用户认证。具体的说,UsernamePasswordAuthenticationFilter提供了基于web form提交参数的认证机制。默认情况下,它处理form表单提交来的username与password两个参数。

2)FilterSecurityInterceptor:FilterSecurityInterceptor是真正直接负责处理关于HTTP资源的安全保护问题的部件,具体说就是处理安全授权问题的部件。其主要过程为:拦截Authentication对"secure object"的访问,依据“安全元信息属性”列表来决定Authentication对象是否可以访问"secure object"。

对于Spring Secuirty而言,“security object”通常只有两大类东西: “方法调用”和“Web请求”。对这两种东西的安全控制都是通过AOP的方法来实施的。

三、Spring Security 项目实践

1、Spring Security 配置文件

1)重写 #configure(AuthenticationManagerBuilder auth) 方法,实现 AuthenticationManager 认证管理器。

2)重写 #configure(HttpSecurity httpSecurity) 方法,主要配置 URL 的权限控制。

3)重写 #authenticationManagerBean 方法,解决无法直接注入 AuthenticationManager 的问题。

4)需设置prePostEnabled = true,否则方法上注解不起作用。


@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
    /**
     * 自定义用户认证逻辑
     */
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 认证失败处理类
     */
    @Autowired
    private AuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 退出处理类
     */
    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;

    /**
     * token认证过滤器
     */
    @Autowired
    private JwtAuthenticationTokenFilter authenticationTokenFilter;

    /**
     * 跨域过滤器
     */
    @Autowired
    private CorsFilter corsFilter;

    /**
     * 允许匿名访问的地址
     */
    @Autowired
    private PermitAllUrlProperties permitAllUrl;

    @Autowired
    private CasProperties casProperties;

    @Autowired
    private CasUserDetailsService customUserDetailsService;

    @Autowired
    private CasAuthenticationSuccessHandler casAuthenticationSuccessHandler;

    @Resource
    private List authorizeRequestsCustomizers;

    /**
     * 解决 无法直接注入 AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception
    {
        // 注解标记允许匿名访问的url
        ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
        permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());

        if (!casProperties.isCasEnable()) {
            httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 禁用HTTP响应标头
                .headers().cacheControl().disable().and()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 每个项目的自定义规则
                .authorizeRequests(registry1 -> // 下面,循环设置自定义规则
                   authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry1)))
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                .antMatchers("/login","/*/goViewLogin", "/**/goview/data/**", "/register", "/captchaImage", "/dev-api/logout").permitAll()
                .antMatchers("/mobile/login/**").permitAll()
                .antMatchers("/app/basicinfo/**").permitAll()
                .antMatchers("/common/upload").permitAll()
                // 静态资源,可匿名访问
                .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
                .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
                .antMatchers("/modeler/**", "/process/**","/definition/**", "/websocket/**", "/jmreport/**", "/file/**").permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();
            // 添加Logout filter
            httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
            // 添加JWT filter
            httpSecurity.addFilterBefore(authenticationTokenFilter,
                UsernamePasswordAuthenticationFilter.class);
            // 添加CORS filter
            httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
            httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
        }
    }

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception
    {
        if (!casProperties.isCasEnable()) {
            auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
        }
        // cas
        if (casProperties.isCasEnable()) {
            super.configure(auth);
            auth.authenticationProvider(casAuthenticationProvider());
        }
    }

    /**
     * 认证的入口
     */
    @Bean
    public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
        CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();
        casAuthenticationEntryPoint.setLoginUrl(casProperties.getCasServerLoginUrl());
        casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
        return casAuthenticationEntryPoint;
    }

    /**
     * 指定service相关信息
     */
    @Bean
    public ServiceProperties serviceProperties() {
        ServiceProperties serviceProperties = new ServiceProperties();
        serviceProperties.setService(casProperties.getAppServerUrl() + casProperties.getAppLoginUrl());
        serviceProperties.setAuthenticateAllArtifacts(true);
        return serviceProperties;
    }

    /**
     * CAS认证过滤器
     */
    @Bean
    public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
        CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
        casAuthenticationFilter.setAuthenticationManager(authenticationManager());
        casAuthenticationFilter.setFilterProcessesUrl(casProperties.getAppLoginUrl());
        casAuthenticationFilter.setAuthenticationSuccessHandler(casAuthenticationSuccessHandler);
        return casAuthenticationFilter;
    }

    /**
     * cas 认证 Provider
     */
    @Bean
    public CasAuthenticationProvider casAuthenticationProvider() {
        CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
        casAuthenticationProvider.setAuthenticationUserDetailsService(customUserDetailsService);
        casAuthenticationProvider.setServiceProperties(serviceProperties());
        casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
        casAuthenticationProvider.setKey("casAuthenticationProviderKey");
        return casAuthenticationProvider;
    }

    @Bean
    public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
        return new Cas20ServiceTicketValidator(casProperties.getCasServerUrl());
    }

    /**
     * 单点登出过滤器
     */
    @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;
    }
}

这里有个userDetailsService接口,我们需要实现它,这里也使用了bCryptPasswordEncoder 强散列哈希加密实现。

在Url权限访问链中,我们配置了业务的规则,这里也配置了Jwt的访问验证。登录那里会返回token,这里就形成闭环。我们项目中前后端用到了Jwt。这里可以参考Jwt相关内容。

2、回到登录

从controller进入到service中,这里有login的具体逻辑。

调用 Spring Security 的 AuthenticationManager 的 authenticate(UsernamePasswordAuthenticationToken authentication) 方法,基于用户名与密码的登录认证。在其内部,会调用我们定义的 UserDetailsServiceImpl 的 loadUserByUsername(String username) 方法,获得指定用户名对应的用户信息。

然后调用 TokenService 的 createToken(LoginUser loginUser) 方法,给认证通过的用户,生成其对应的认证 token。

在UserDetailsServiceImpl 中,实现 Spring Security UserDetailsService 接口,实现了该接口定义的 loadUserByUsername(String username) 方法,获得指定用户名对应的用户信息。这里MenuPermission(SysUser user) 方法,获得用户的 SysRoleMenu 的权限标识字符串的集合。

通过 Spring Security 提供的 @PreAuthorize 注解,实现基于 Spring EL 表达式的执行结果为 true 时,可以访问,从而实现灵活的权限校验。

通过 @PreAuthorize 注解的特性,使用其 PermissionService 提供的权限验证的方法。

这里会有一个 @ss 呢?在 Spring EL 表达式中,调用指定 Bean 名字的方法时,使用 @ + Bean 的名字,声明 PermissionService 的 Bean 名字为 ss 。

在controller中与之上对应:

// SysDictDataController.java
@PreAuthorize("@ss.hasPermi('system:dict:list')")
@GetMapping("/list")
展开阅读全文

页面更新:2024-05-23

标签:项目   注解   过滤器   框架   角色   权限   对象   参数   方法   系列   用户

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top