security_securityutils - CSDN
security 订阅
网络安全技术及其协议,包括了网络通信安全、信息在网络传输中的保密性和完整性、控制访问受限网域与敏感信息以及在公共网络如因特网上使用隐秘通讯。为了解决这些问题,各大组织及技术供应商纷纷推出了各种网络和信息安全技术。 展开全文
网络安全技术及其协议,包括了网络通信安全、信息在网络传输中的保密性和完整性、控制访问受限网域与敏感信息以及在公共网络如因特网上使用隐秘通讯。为了解决这些问题,各大组织及技术供应商纷纷推出了各种网络和信息安全技术。
信息
外文名
Security
包括了
网络通信安全
中文名
网络安全技术及其协议
推出了
各种网络和信息安全技术
Security基本介绍
Security:网络安全技术及其协议(Network Security Technologies and Protocols:AAA,VPN and Firewall)
收起全文
精华内容
参与话题
  • 手把手带你入门 Spring Security

    千次阅读 2019-07-25 08:52:07
    Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直是 Shiro 的天下。 相对于 Shiro,在 SSM...

    Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直是 Shiro 的天下。

    相对于 Shiro,在 SSM/SSH 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有 Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。

    自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了 自动化配置方案,可以零配置使用 Spring Security。

    因此,一般来说,常见的安全管理技术栈的组合是这样的:

    • SSM + Shiro
    • Spring Boot/Spring Cloud + Spring Security

    注意,这只是一个推荐的组合而已,如果单纯从技术上来说,无论怎么组合,都是可以运行的。

    我们来看下具体使用。

    1.项目创建

    在 Spring Boot 中使用 Spring Security 非常容易,引入依赖即可:

    pom.xml 中的 Spring Security 依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    

    只要加入依赖,项目的所有接口都会被自动保护起来。

    2.初次体验

    我们创建一个 HelloController:

    @RestController
    public class HelloController {
        @GetMapping("/hello")
        public String hello() {
            return "hello";
        }
    }
    

    访问 /hello ,需要登录之后才能访问。

    当用户从浏览器发送请求访问 /hello 接口时,服务端会返回 302 响应码,让客户端重定向到 /login 页面,用户在 /login 页面登录,登陆成功之后,就会自动跳转到 /hello 接口。

    另外,也可以使用 POSTMAN 来发送请求,使用 POSTMAN 发送请求时,可以将用户信息放在请求头中(这样可以避免重定向到登录页面):

    通过以上两种不同的登录方式,可以看出,Spring Security 支持两种不同的认证方式:

    • 可以通过 form 表单来认证
    • 可以通过 HttpBasic 来认证

    3.用户名配置

    默认情况下,登录的用户名是 user ,密码则是项目启动时随机生成的字符串,可以从启动的控制台日志中看到默认密码:

    这个随机生成的密码,每次启动时都会变。对登录的用户名/密码进行配置,有三种不同的方式:

    • 在 application.properties 中进行配置
    • 通过 Java 代码配置在内存中
    • 通过 Java 从数据库中加载

    前两种比较简单,第三种代码量略大,本文就先来看看前两种,第三种后面再单独写文章介绍,也可以参考我的微人事项目

    3.1 配置文件配置用户名/密码

    可以直接在 application.properties 文件中配置用户的基本信息:

    spring.security.user.name=javaboy
    spring.security.user.password=123
    

    配置完成后,重启项目,就可以使用这里配置的用户名/密码登录了。

    3.2 Java 配置用户名/密码

    也可以在 Java 代码中配置用户名密码,首先需要我们创建一个 Spring Security 的配置类,集成自 WebSecurityConfigurerAdapter 类,如下:

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            //下面这两行配置表示在内存中配置了两个用户
            auth.inMemoryAuthentication()
                    .withUser("javaboy").roles("admin").password("$2a$10$OR3VSksVAmCzc.7WeaRPR.t0wyCsIj24k0Bne8iKWV1o.V9wsP8Xe")
                    .and()
                    .withUser("lisi").roles("user").password("$2a$10$p1H8iWa8I4.CA.7Z8bwLjes91ZpY.rYREGHQEInNtAp4NzL6PLKxi");
        }
        @Bean
        PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }
    

    这里我们在 configure 方法中配置了两个用户,用户的密码都是加密之后的字符串(明文是 123),从 Spring5 开始,强制要求密码要加密,如果非不想加密,可以使用一个过期的 PasswordEncoder 的实例 NoOpPasswordEncoder,但是不建议这么做,毕竟不安全。

    Spring Security 中提供了 BCryptPasswordEncoder 密码编码工具,可以非常方便的实现密码的加密加盐,相同明文加密出来的结果总是不同,这样就不需要用户去额外保存的字段了,这一点比 Shiro 要方便很多。

    4.登录配置

    对于登录接口,登录成功后的响应,登录失败后的响应,我们都可以在 WebSecurityConfigurerAdapter 的实现类中进行配置。例如下面这样:

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        VerifyCodeFilter verifyCodeFilter;
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);
            http
            .authorizeRequests()//开启登录配置
            .antMatchers("/hello").hasRole("admin")//表示访问 /hello 这个接口,需要具备 admin 这个角色
            .anyRequest().authenticated()//表示剩余的其他接口,登录之后就能访问
            .and()
            .formLogin()
            //定义登录页面,未登录时,访问一个需要登录之后才能访问的接口,会自动跳转到该页面
            .loginPage("/login_p")
            //登录处理接口
            .loginProcessingUrl("/doLogin")
            //定义登录时,用户名的 key,默认为 username
            .usernameParameter("uname")
            //定义登录时,用户密码的 key,默认为 password
            .passwordParameter("passwd")
            //登录成功的处理器
            .successHandler(new AuthenticationSuccessHandler() {
                @Override
                public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter out = resp.getWriter();
                        out.write("success");
                        out.flush();
                    }
                })
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException exception) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter out = resp.getWriter();
                        out.write("fail");
                        out.flush();
                    }
                })
                .permitAll()//和表单登录相关的接口统统都直接通过
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessHandler(new LogoutSuccessHandler() {
                    @Override
                    public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter out = resp.getWriter();
                        out.write("logout success");
                        out.flush();
                    }
                })
                .permitAll()
                .and()
                .httpBasic()
                .and()
                .csrf().disable();
        }
    }
    

    我们可以在 successHandler 方法中,配置登录成功的回调,如果是前后端分离开发的话,登录成功后返回 JSON 即可,同理,failureHandler 方法中配置登录失败的回调,logoutSuccessHandler 中则配置注销成功的回调。

    5.忽略拦截

    如果某一个请求地址不需要拦截的话,有两种方式实现:

    • 设置该地址匿名访问
    • 直接过滤掉该地址,即该地址不走 Spring Security 过滤器链

    推荐使用第二种方案,配置如下:

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/vercode");
        }
    }
    

    Spring Security 另外一个强大之处就是它可以结合 OAuth2 ,玩出更多的花样出来,这些我们在后面的文章中再和大家细细介绍。

    本文就先说到这里,有问题欢迎留言讨论。

    关注公众号【江南一点雨】,专注于 Spring Boot+微服务以及前后端分离等全栈技术,定期视频教程分享,关注后回复 Java ,领取松哥为你精心准备的 Java 干货!

    展开全文
  • spring security——基本介绍(一)

    万次阅读 多人点赞 2019-12-11 16:11:57
    一、spring security 简介 spring security 的核心功能主要包括: 认证 (你是谁) 授权 (你能干什么) 攻击防护 (防止伪造身份) 其核心就是一组过滤器链,项目启动后将会自动配置。最核心的就是 Basic ...

    一、spring security 简介

            spring security 的核心功能主要包括:

    • 认证 (你是谁)
    • 授权 (你能干什么)
    • 攻击防护 (防止伪造身份)

         其核心就是一组过滤器链,项目启动后将会自动配置。最核心的就是 Basic Authentication Filter 用来认证用户的身份,一个在spring security中一种过滤器处理一种认证方式。

    比如,对于username password认证过滤器来说, 

    会检查是否是一个登录请求;

    是否包含username 和 password (也就是该过滤器需要的一些认证信息) ;

    如果不满足则放行给下一个。

         下一个按照自身职责判定是否是自身需要的信息,basic的特征就是在请求头中有 Authorization:Basic eHh4Onh4 的信息。中间可能还有更多的认证过滤器。最后一环是 FilterSecurityInterceptor,这里会判定该请求是否能进行访问rest服务,判断的依据是 BrowserSecurityConfig中的配置,如果被拒绝了就会抛出不同的异常(根据具体的原因)。Exception Translation Filter 会捕获抛出的错误,然后根据不同的认证方式进行信息的返回提示。

    注意:绿色的过滤器可以配置是否生效,其他的都不能控制。

    二、入门项目

         首先创建spring boot项目HelloSecurity,其pom主要依赖如下:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    然后在src/main/resources/templates/目录下创建页面:

    home.html

    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
        <head>
            <title>Spring Security Example</title>
        </head>
        <body>
            <h1>Welcome!</h1>
    ​
            <p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p>
        </body>
    </html>

    我们可以看到, 在这个简单的视图中包含了一个链接: “/hello”. 链接到了如下的页面,Thymeleaf模板如下:

    hello.html

    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
          xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
        <head>
            <title>Hello World!</title>
        </head>
        <body>
            <h1>Hello world!</h1>
        </body>
    </html>

    Web应用程序基于Spring MVC。 因此,你需要配置Spring MVC并设置视图控制器来暴露这些模板。 如下是一个典型的Spring MVC配置类。在src/main/java/hello目录下(所以java都在这里):

    @Configuration
    public class MvcConfig extends WebMvcConfigurerAdapter {
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/home").setViewName("home");
            registry.addViewController("/").setViewName("home");
            registry.addViewController("/hello").setViewName("hello");
            registry.addViewController("/login").setViewName("login");
        }
    }

         addViewControllers()方法(覆盖WebMvcConfigurerAdapter中同名的方法)添加了四个视图控制器。 两个视图控制器引用名称为“home”的视图(在home.html中定义),另一个引用名为“hello”的视图(在hello.html中定义)。 第四个视图控制器引用另一个名为“login”的视图。 将在下一部分中创建该视图。此时,可以跳过来使应用程序可执行并运行应用程序,而无需登录任何内容。然后启动程序如下:

    @SpringBootApplication
    public class Application {
    ​
        public static void main(String[] args) throws Throwable {
            SpringApplication.run(Application.class, args);
        }
    }

    2、加入Spring Security

         假设你希望防止未经授权的用户访问“/ hello”。 此时,如果用户点击主页上的链接,他们会看到问候语,请求被没有被拦截。 你需要添加一个障碍,使得用户在看到该页面之前登录。您可以通过在应用程序中配置Spring Security来实现。 如果Spring Security在类路径上,则Spring Boot会使用“Basic认证”来自动保护所有HTTP端点。 同时,你可以进一步自定义安全设置。首先在pom文件中引入:

    <dependencies>
        ...
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
        ...
    </dependencies>

    如下是安全配置,使得只有认证过的用户才可以访问到问候页面:

    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests()
                    .antMatchers("/", "/home").permitAll()
                    .anyRequest().authenticated()
                    .and()
                .formLogin()
                    .loginPage("/login")
                    .permitAll()
                    .and()
                .logout()
                    .permitAll();
        }
    ​
        @Autowired
        public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
            auth
                .inMemoryAuthentication()
                    .withUser("user").password("password").roles("USER");
        }
    }

         WebSecurityConfig类使用了@EnableWebSecurity注解 ,以启用Spring Security的Web安全支持,并提供Spring MVC集成。它还扩展了WebSecurityConfigurerAdapter,并覆盖了一些方法来设置Web安全配置的一些细节。

         configure(HttpSecurity)方法定义了哪些URL路径应该被保护,哪些不应该。具体来说,“/”和“/ home”路径被配置为不需要任何身份验证。所有其他路径必须经过身份验证。

         当用户成功登录时,它们将被重定向到先前请求的需要身份认证的页面。有一个由 loginPage()指定的自定义“/登录”页面,每个人都可以查看它。

         对于configureGlobal(AuthenticationManagerBuilder) 方法,它将单个用户设置在内存中。该用户的用户名为“user”,密码为“password”,角色为“USER”。

         现在我们需要创建登录页面。前面我们已经配置了“login”的视图控制器,因此现在只需要创建登录页面即可:

    login.html

    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
          xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
        <head>
            <title>Spring Security Example </title>
        </head>
        <body>
            <div th:if="${param.error}">
                Invalid username and password.
            </div>
            <div th:if="${param.logout}">
                You have been logged out.
            </div>
            <form th:action="@{/login}" method="post">
                <div><label> User Name : <input type="text" name="username"/> </label></div>
                <div><label> Password: <input type="password" name="password"/> </label></div>
                <div><input type="submit" value="Sign In"/></div>
            </form>
        </body>
    </html>

         你可以看到,这个Thymeleaf模板只是提供一个表单来获取用户名和密码,并将它们提交到“/ login”。 根据配置,Spring Security提供了一个拦截该请求并验证用户的过滤器。 如果用户未通过认证,该页面将重定向到“/ login?error”,并在页面显示相应的错误消息。 注销成功后,我们的应用程序将发送到“/ login?logout”,我们的页面显示相应的登出成功消息。最后,我们需要向用户提供一个显示当前用户名和登出的方法。 更新hello.html 向当前用户打印一句hello,并包含一个“注销”表单,如下所示:

    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
          xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
        <head>
            <title>Hello World!</title>
        </head>
        <body>
            <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
            <form th:action="@{/logout}" method="post">
                <input type="submit" value="Sign Out"/>
            </form>
        </body>
    </html>

    三、参数详解

    1、注解 @EnableWebSecurity

         在 Spring boot 应用中使用 Spring Security,用到了 @EnableWebSecurity注解,官方说明为,该注解和 @Configuration 注解一起使用, 注解 WebSecurityConfigurer 类型的类,或者利用@EnableWebSecurity 注解继承 WebSecurityConfigurerAdapter的类,这样就构成了 Spring Security 的配置。

    2、抽象类 WebSecurityConfigurerAdapter

         一般情况,会选择继承 WebSecurityConfigurerAdapter 类,其官方说明为:WebSecurityConfigurerAdapter 提供了一种便利的方式去创建 WebSecurityConfigurer的实例,只需要重写 WebSecurityConfigurerAdapter 的方法,即可配置拦截什么URL、设置什么权限等安全控制。

    3、方法 configure(AuthenticationManagerBuilder auth) 和 configure(HttpSecurity http)

         Demo 中重写了 WebSecurityConfigurerAdapter 的两个方法:

       /**
         * 通过 {@link #authenticationManager()} 方法的默认实现尝试获取一个 {@link AuthenticationManager}.
         * 如果被复写, 应该使用{@link AuthenticationManagerBuilder} 来指定 {@link AuthenticationManager}.
         *
         * 例如, 可以使用以下配置在内存中进行注册公开内存的身份验证{@link UserDetailsService}:
         *
         * // 在内存中添加 user 和 admin 用户
         * @Override
         * protected void configure(AuthenticationManagerBuilder auth) {
         *     auth
         *       .inMemoryAuthentication().withUser("user").password("password").roles("USER").and()
         *         .withUser("admin").password("password").roles("USER", "ADMIN");
         * }
         *
         * // 将 UserDetailsService 显示为 Bean
         * @Bean
         * @Override
         * public UserDetailsService userDetailsServiceBean() throws Exception {
         *     return super.userDetailsServiceBean();
         * }
         *
         */
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            this.disableLocalConfigureAuthenticationBldr = true;
        }
    
    
        /**
         * 复写这个方法来配置 {@link HttpSecurity}. 
         * 通常,子类不能通过调用 super 来调用此方法,因为它可能会覆盖其配置。 默认配置为:
         * 
         * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
         *
         */
        protected void configure(HttpSecurity http) throws Exception {
            logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
    ​
            http
                .authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                .formLogin().and()
                .httpBasic();
        }

    4、final 类 HttpSecurity

    HttpSecurity 常用方法及说明:

    方法 说明
    openidLogin() 用于基于 OpenId 的验证
    headers() 将安全标头添加到响应
    cors() 配置跨域资源共享( CORS )
    sessionManagement() 允许配置会话管理
    portMapper() 允许配置一个PortMapper(HttpSecurity#(getSharedObject(class))),其他提供SecurityConfigurer的对象使用 PortMapper 从 HTTP 重定向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security使用一个PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口8443,HTTP 端口80到 HTTPS 端口443
    jee() 配置基于容器的预认证。 在这种情况下,认证由Servlet容器管理
    x509() 配置基于x509的认证
    rememberMe 允许配置“记住我”的验证
    authorizeRequests() 允许基于使用HttpServletRequest限制访问
    requestCache() 允许配置请求缓存
    exceptionHandling() 允许配置错误处理
    securityContext() HttpServletRequests之间的SecurityContextHolder上设置SecurityContext的管理。 当使用WebSecurityConfigurerAdapter时,这将自动应用
    servletApi() HttpServletRequest方法与在其上找到的值集成到SecurityContext中。 当使用WebSecurityConfigurerAdapter时,这将自动应用
    csrf() 添加 CSRF 支持,使用WebSecurityConfigurerAdapter时,默认启用
    logout() 添加退出登录支持。当使用WebSecurityConfigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来清除用户,清除已配置的任何#rememberMe()身份验证,清除SecurityContextHolder,然后重定向到”/login?success”
    anonymous() 允许配置匿名用户的表示方法。 当与WebSecurityConfigurerAdapter结合使用时,这将自动应用。 默认情况下,匿名用户将使用org.springframework.security.authentication.AnonymousAuthenticationToken表示,并包含角色 “ROLE_ANONYMOUS”
    formLogin() 指定支持基于表单的身份验证。如果未指定FormLoginConfigurer#loginPage(String),则将生成默认登录页面
    oauth2Login() 根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证
    requiresChannel() 配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射
    httpBasic() 配置 Http Basic 验证
    addFilterAt() 在指定的Filter类的位置添加过滤器

    5、类 AuthenticationManagerBuilder

    /**
    * {@link SecurityBuilder} used to create an {@link AuthenticationManager}. Allows for
    * easily building in memory authentication, LDAP authentication, JDBC based
    * authentication, adding {@link UserDetailsService}, and adding
    * {@link AuthenticationProvider}'s.
    */

            意思是,AuthenticationManagerBuilder 用于创建一个 AuthenticationManager,让我能够轻松的实现内存验证、LADP验证、基于JDBC的验证、添加UserDetailsService、添加AuthenticationProvider。

    四、原理讲解

    1、校验流程图

     

    2、源码分析

    • AbstractAuthenticationProcessingFilter 抽象类
        public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
                throws IOException, ServletException {
    ​
            HttpServletRequest request = (HttpServletRequest) req;
            HttpServletResponse response = (HttpServletResponse) res;
    ​
            if (!requiresAuthentication(request, response)) {
                chain.doFilter(request, response);
    ​
                return;
            }
    ​
            if (logger.isDebugEnabled()) {
                logger.debug("Request is to process authentication");
            }
    ​
            Authentication authResult;
    ​
            try {
                authResult = attemptAuthentication(request, response);
                if (authResult == null) {
                    // return immediately as subclass has indicated that it hasn't completed
                    // authentication
                    return;
                }
                sessionStrategy.onAuthentication(authResult, request, response);
            }
            catch (InternalAuthenticationServiceException failed) {
                logger.error(
                        "An internal error occurred while trying to authenticate the user.",
                        failed);
                unsuccessfulAuthentication(request, response, failed);
    ​
                return;
            }
            catch (AuthenticationException failed) {
                // Authentication failed
                unsuccessfulAuthentication(request, response, failed);
    ​
                return;
            }
    ​
            // Authentication success
            if (continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }
    ​
            successfulAuthentication(request, response, chain, authResult);
        }

            调用 requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。如果需要验证,则会调用 attemptAuthentication(HttpServletRequest, HttpServletResponse) 方法,有三种结果:

    1. 返回一个 Authentication 对象。配置的 SessionAuthenticationStrategy` 将被调用,然后 然后调用 successfulAuthentication(HttpServletRequest,HttpServletResponse,FilterChain,Authentication) 方法。
    2. 验证时发生 AuthenticationException。unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException) 方法将被调用。
    3. 返回Null,表示身份验证不完整。假设子类做了一些必要的工作(如重定向)来继续处理验证,方法将立即返回。假设后一个请求将被这种方法接收,其中返回的Authentication对象不为空。
    • UsernamePasswordAuthenticationFilter(AbstractAuthenticationProcessingFilter的子类)
    public Authentication attemptAuthentication(HttpServletRequest request,
                HttpServletResponse response) throws AuthenticationException {
            if (postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException(
                        "Authentication method not supported: " + request.getMethod());
            }
    ​
            String username = obtainUsername(request);
            String password = obtainPassword(request);
    ​
            if (username == null) {
                username = "";
            }
    ​
            if (password == null) {
                password = "";
            }
    ​
            username = username.trim();
    ​
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    username, password);
    ​
            // Allow subclasses to set the "details" property
            setDetails(request, authRequest);
    ​
            return this.getAuthenticationManager().authenticate(authRequest);
        }

            attemptAuthentication () 方法将 request 中的 username 和 password 生成 UsernamePasswordAuthenticationToken 对象,用于 AuthenticationManager 的验证(即 this.getAuthenticationManager().authenticate(authRequest) )。默认情况下注入 Spring 容器的 AuthenticationManager 是 ProviderManager。

    • ProviderManager(AuthenticationManager的实现类)
    public Authentication authenticate(Authentication authentication)
        throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        Authentication result = null;
        boolean debug = logger.isDebugEnabled();
    ​
        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }
    ​
            if (debug) {
                logger.debug("Authentication attempt using "
                             + provider.getClass().getName());
            }
    ​
            try {
                result = provider.authenticate(authentication);
    ​
                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
            catch (AccountStatusException e) {
                prepareException(e, authentication);
                // SEC-546: Avoid polling additional providers if auth failure is due to
                // invalid account status
                throw e;
            }
            catch (InternalAuthenticationServiceException e) {
                prepareException(e, authentication);
                throw e;
            }
            catch (AuthenticationException e) {
                lastException = e;
            }
        }
    ​
        if (result == null && parent != null) {
            // Allow the parent to try.
            try {
                result = parent.authenticate(authentication);
            }
            catch (ProviderNotFoundException e) {
                // ignore as we will throw below if no other exception occurred prior to
                // calling parent and the parent
                // may throw ProviderNotFound even though a provider in the child already
                // handled the request
            }
            catch (AuthenticationException e) {
                lastException = e;
            }
        }
    ​
        if (result != null) {
            if (eraseCredentialsAfterAuthentication
                && (result instanceof CredentialsContainer)) {
                // Authentication is complete. Remove credentials and other secret data
                // from authentication
                ((CredentialsContainer) result).eraseCredentials();
            }
    ​
            eventPublisher.publishAuthenticationSuccess(result);
            return result;
        }
    ​
        // Parent was null, or didn't authenticate (or throw an exception).
    ​
        if (lastException == null) {
            lastException = new ProviderNotFoundException(messages.getMessage(
                "ProviderManager.providerNotFound",
                new Object[] { toTest.getName() },
                "No AuthenticationProvider found for {0}"));
        }
    ​
        prepareException(lastException, authentication);
    ​
        throw lastException;
    }

            尝试验证 Authentication 对象。AuthenticationProvider 列表将被连续尝试,直到 AuthenticationProvider 表示它能够认证传递的过来的Authentication 对象。然后将使用该 AuthenticationProvider 尝试身份验证。如果有多个 AuthenticationProvider 支持验证传递过来的Authentication 对象,那么由第一个来确定结果,覆盖早期支持AuthenticationProviders 所引发的任何可能的AuthenticationException。 成功验证后,将不会尝试后续的AuthenticationProvider。如果最后所有的 AuthenticationProviders 都没有成功验证 Authentication 对象,将抛出 AuthenticationException。从代码中不难看出,由 provider 来验证 authentication, 核心点方法是:

    Authentication result = provider.authenticate(authentication);

    此处的 provider 是 AbstractUserDetailsAuthenticationProvider,AbstractUserDetailsAuthenticationProvider 是AuthenticationProvider的实现,看看它的 authenticate(authentication) 方法:

    // 验证 authentication
    public Authentication authenticate(Authentication authentication)
                throws AuthenticationException {
            Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                    messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.onlySupports",
                            "Only UsernamePasswordAuthenticationToken is supported"));
    ​
            // Determine username
            String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                    : authentication.getName();
    ​
            boolean cacheWasUsed = true;
            UserDetails user = this.userCache.getUserFromCache(username);
    ​
            if (user == null) {
                cacheWasUsed = false;
    ​
                try {
                    user = retrieveUser(username,
                            (UsernamePasswordAuthenticationToken) authentication);
                }
                catch (UsernameNotFoundException notFound) {
                    logger.debug("User '" + username + "' not found");
    ​
                    if (hideUserNotFoundExceptions) {
                        throw new BadCredentialsException(messages.getMessage(
                                "AbstractUserDetailsAuthenticationProvider.badCredentials",
                                "Bad credentials"));
                    }
                    else {
                        throw notFound;
                    }
                }
    ​
                Assert.notNull(user,
                        "retrieveUser returned null - a violation of the interface contract");
            }
    ​
            try {
                preAuthenticationChecks.check(user);
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (AuthenticationException exception) {
                if (cacheWasUsed) {
                    // There was a problem, so try again after checking
                    // we're using latest data (i.e. not from the cache)
                    cacheWasUsed = false;
                    user = retrieveUser(username,
                            (UsernamePasswordAuthenticationToken) authentication);
                    preAuthenticationChecks.check(user);
                    additionalAuthenticationChecks(user,
                            (UsernamePasswordAuthenticationToken) authentication);
                }
                else {
                    throw exception;
                }
            }
    ​
            postAuthenticationChecks.check(user);
    ​
            if (!cacheWasUsed) {
                this.userCache.putUserInCache(user);
            }
    ​
            Object principalToReturn = user;
    ​
            if (forcePrincipalAsString) {
                principalToReturn = user.getUsername();
            }
    ​
            return createSuccessAuthentication(principalToReturn, authentication, user);
        }

    AbstractUserDetailsAuthenticationProvider 内置了缓存机制,从缓存中获取不到的 UserDetails 信息的话,就调用如下方法获取用户信息,然后和 用户传来的信息进行对比来判断是否验证成功。

    // 获取用户信息
    UserDetails user = retrieveUser(username,
     (UsernamePasswordAuthenticationToken) authentication);

    retrieveUser() 方法在 DaoAuthenticationProvider 中实现,DaoAuthenticationProvider 是 AbstractUserDetailsAuthenticationProvider的子类。具体实现如下:

    protected final UserDetails retrieveUser(String username,
                UsernamePasswordAuthenticationToken authentication)
                throws AuthenticationException {
            UserDetails loadedUser;
    ​
            try {
                loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            }
            catch (UsernameNotFoundException notFound) {
                if (authentication.getCredentials() != null) {
                    String presentedPassword = authentication.getCredentials().toString();
                    passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,
                            presentedPassword, null);
                }
                throw notFound;
            }
            catch (Exception repositoryProblem) {
                throw new InternalAuthenticationServiceException(
                        repositoryProblem.getMessage(), repositoryProblem);
            }
    ​
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        }

    可以看到此处的返回对象 userDetails 是由 UserDetailsService 的 #loadUserByUsername(username) 来获取的。

    展开全文
  • SpringBoot集成Spring Security(1)——入门程序

    万次阅读 多人点赞 2020-05-22 18:49:31
    因为项目需要,第一次接触Spring Security,早就听闻Spring Security强大但上手困难,今天学习了一天,翻遍了全网资料,才仅仅出入门道,特整理这篇文章来让后来者少踩一点坑(本文附带实例程序,请放心食用) ...

    因为项目需要,第一次接触 Spring Security,早就听闻 Spring Security 功能强大但上手困难,学习了几天出入门道,特整理这篇文章希望能让后来者少踩一点坑(本文附带实例程序,请放心食用

    本篇文章环境:SpringBoot 2.0 + Mybatis + Spring Security 5.0

    注意:SpringSecurity 5.0+ 版本变动较多,且不兼容之前版本,确保你的 SpringBoot 版本为 2.0,能帮你避免掉大部分的坑。

    源码地址:https://github.com/jitwxs/blog_sample

    一、导入依赖

    导入 spring-boot-starter-security 依赖,在 SpringBoot 2.0 环境下默认使用的是 5.0 版本。

    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-test</artifactId>
    	<scope>test</scope>
    </dependency>
    
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
    <dependency>
    	<groupId>org.mybatis.spring.boot</groupId>
    	<artifactId>mybatis-spring-boot-starter</artifactId>
    	<version>1.3.1</version>
    </dependency>
    
    <dependency>
    	<groupId>mysql</groupId>
    	<artifactId>mysql-connector-java</artifactId>
    </dependency>
    

    二、创建数据库

    一般权限控制有三层,即:用户<–>角色<–>权限,用户与角色是多对多,角色和权限也是多对多。这里我们先暂时不考虑权限,只考虑用户<–>角色

    创建用户表sys_user

    CREATE TABLE `sys_user` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(255) NOT NULL,
      `password` varchar(255) NOT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    

    创建权限表sys_role

    CREATE TABLE `sys_role` (
      `id` int(11) NOT NULL,
      `name` varchar(255) NOT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    

    创建用户-角色表sys_user_role

    CREATE TABLE `sys_user_role` (
      `user_id` int(11) NOT NULL,
      `role_id` int(11) NOT NULL,
      PRIMARY KEY (`user_id`,`role_id`),
      KEY `fk_role_id` (`role_id`),
      CONSTRAINT `fk_role_id` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
      CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    

    初始化一下数据:

    INSERT INTO `sys_role` VALUES ('1', 'ROLE_ADMIN');
    INSERT INTO `sys_role` VALUES ('2', 'ROLE_USER');
    
    INSERT INTO `sys_user` VALUES ('1', 'admin', '123');
    INSERT INTO `sys_user` VALUES ('2', 'jitwxs', '123');
    
    INSERT INTO `sys_user_role` VALUES ('1', '1');
    INSERT INTO `sys_user_role` VALUES ('2', '2');
    

    博主有话说:

    这里的权限格式为ROLE_XXX,是Spring Security规定的,不要乱起名字哦。

    三、准备页面

    因为是示例程序,页面越简单越好,只用于登陆的login.html以及用于登陆成功后的home.html,将其放置在 resources/static 目录下:

    (1)login.html:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>登陆</title>
    </head>
    <body>
    <h1>登陆</h1>
    <form method="post" action="/login">
        <div>
            用户名:<input type="text" name="username">
        </div>
        <div>
            密码:<input type="password" name="password">
        </div>
        <div>
            <button type="submit">立即登陆</button>
        </div>
    </form>
    </body>
    </html>
    

    博主有话说:

    用户的登陆认证是由Spring Security进行处理的,请求路径默认为/login,用户名字段默认为username,密码字段默认为password

    (2)home.html:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <h1>登陆成功</h1>
        <a href="/admin">检测ROLE_ADMIN角色</a>
        <a href="/user">检测ROLE_USER角色</a>
        <button onclick="window.location.href='/logout'">退出登录</button>
    </body>
    </html>
    

    四、配置application.properties

    在配置文件中配置下数据库连接:

    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=utf-8&useSSL=true
    spring.datasource.username=root
    spring.datasource.password=root
    
    #开启Mybatis下划线命名转驼峰命名
    mybatis.configuration.map-underscore-to-camel-case=true
    

    五、创建实体、Dao、Service和Controller

    5.1 实体

    (1)SysUser

    public class SysUser implements Serializable{
        static final long serialVersionUID = 1L;
    
        private Integer id;
    
        private String name;
    
        private String password;
    
        // 省略getter/setter
    }
    

    (2)SysRole

    public class SysRole implements Serializable {
        static final long serialVersionUID = 1L;
    
        private Integer id;
    
        private String name;
    
        // 省略getter/setter
    }
    

    (3)SysUserRole

    public class SysUserRole implements Serializable {
        static final long serialVersionUID = 1L;
    
        private Integer userId;
    
        private Integer roleId;
    	
        // 省略getter/setter
    }
    

    5.2 Dao

    (1)SysUserMapper

    @Mapper
    public interface SysUserMapper {
        @Select("SELECT * FROM sys_user WHERE id = #{id}")
        SysUser selectById(Integer id);
    
        @Select("SELECT * FROM sys_user WHERE name = #{name}")
        SysUser selectByName(String name);
    }
    

    (2)SysRoleMapper

    @Mapper
    public interface SysRoleMapper {
        @Select("SELECT * FROM sys_role WHERE id = #{id}")
        SysRole selectById(Integer id);
    }
    

    (3)SysUserRoleMapper

    @Mapper
    public interface SysUserRoleMapper {
        @Select("SELECT * FROM sys_user_role WHERE user_id = #{userId}")
        List<SysUserRole> listByUserId(Integer userId);
    }
    

    5.3 Service

    (1)SysUserService

    @Service
    public class SysUserService {
        @Autowired
        private SysUserMapper userMapper;
    
        public SysUser selectById(Integer id) {
            return userMapper.selectById(id);
        }
    
        public SysUser selectByName(String name) {
            return userMapper.selectByName(name);
        }
    }
    

    (2)SysRoleService

    @Service
    public class SysRoleService {
        @Autowired
        private SysRoleMapper roleMapper;
    
        public SysRole selectById(Integer id){
            return roleMapper.selectById(id);
        }
    }
    

    (3)SysUserRoleService

    @Service
    public class SysUserRoleService {
        @Autowired
        private SysUserRoleMapper userRoleMapper;
    
        public List<SysUserRole> listByUserId(Integer userId) {
            return userRoleMapper.listByUserId(userId);
        }
    }
    

    5.4 Controller

    @Controller
    public class LoginController {
        private Logger logger = LoggerFactory.getLogger(LoginController.class);
    
        @RequestMapping("/")
        public String showHome() {
            String name = SecurityContextHolder.getContext().getAuthentication().getName();
            logger.info("当前登陆用户:" + name);
    
            return "home.html";
        }
    
        @RequestMapping("/login")
        public String showLogin() {
            return "login.html";
        }
    
        @RequestMapping("/admin")
        @ResponseBody
        @PreAuthorize("hasRole('ROLE_ADMIN')")
        public String printAdmin() {
            return "如果你看见这句话,说明你有ROLE_ADMIN角色";
        }
    
        @RequestMapping("/user")
        @ResponseBody
        @PreAuthorize("hasRole('ROLE_USER')")
        public String printUser() {
            return "如果你看见这句话,说明你有ROLE_USER角色";
        }
    }
    

    博主有话说:

    • 如代码所示,获取当前登录用户:SecurityContextHolder.getContext().getAuthentication()

    • @PreAuthorize 用于判断用户是否有指定权限,没有就不能访问

    六、配置SpringSecurity

    6.1 UserDetailsService

    首先我们需要自定义 UserDetailsService ,将用户信息和权限注入进来。

    我们需要重写 loadUserByUsername 方法,参数是用户输入的用户名。返回值是UserDetails,这是一个接口,一般使用它的子类org.springframework.security.core.userdetails.User,它有三个参数,分别是用户名、密码和权限集。

    实际情况下,大多将 DAO 中的 User 类继承 org.springframework.security.core.userdetails.User 返回。

    @Service("userDetailsService")
    public class CustomUserDetailsService implements UserDetailsService {
        @Autowired
        private SysUserService userService;
    
        @Autowired
        private SysRoleService roleService;
    
        @Autowired
        private SysUserRoleService userRoleService;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            // 从数据库中取出用户信息
            SysUser user = userService.selectByName(username);
    
            // 判断用户是否存在
            if(user == null) {
                throw new UsernameNotFoundException("用户名不存在");
            }
    
            // 添加权限
            List<SysUserRole> userRoles = userRoleService.listByUserId(user.getId());
            for (SysUserRole userRole : userRoles) {
                SysRole role = roleService.selectById(userRole.getRoleId());
                authorities.add(new SimpleGrantedAuthority(role.getName()));
            }
    
            // 返回UserDetails实现类
            return new User(user.getName(), user.getPassword(), authorities);
        }
    }
    

    6.2 WebSecurityConfig

    该类是 Spring Security 的配置类,该类的三个注解分别是标识该类是配置类、开启 Security 服务、开启全局 Securtiy 注解。

    首先将我们自定义的 userDetailsService 注入进来,在 configure() 方法中使用 auth.userDetailsService() 方法替换掉默认的 userDetailsService。

    这里我们还指定了密码的加密方式(5.0 版本强制要求设置),因为我们数据库是明文存储的,所以明文返回即可,如下所示:

    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private CustomUserDetailsService userDetailsService;
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService).passwordEncoder(new PasswordEncoder() {
                @Override
                public String encode(CharSequence charSequence) {
                    return charSequence.toString();
                }
    
                @Override
                public boolean matches(CharSequence charSequence, String s) {
                    return s.equals(charSequence.toString());
                }
            });
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    // 如果有允许匿名的url,填在下面
    //                .antMatchers().permitAll()
                    .anyRequest().authenticated()
                    .and()
                    // 设置登陆页
                    .formLogin().loginPage("/login")
                    // 设置登陆成功页
                    .defaultSuccessUrl("/").permitAll()
                    // 自定义登陆用户名和密码参数,默认为username和password
    //                .usernameParameter("username")
    //                .passwordParameter("password")
                    .and()
                    .logout().permitAll();
    
            // 关闭CSRF跨域
            http.csrf().disable();
        }
    
        @Override
        public void configure(WebSecurity web) throws Exception {
            // 设置拦截忽略文件夹,可以对静态资源放行
            web.ignoring().antMatchers("/css/**", "/js/**");
        }
    }
    

    七、运行程序

    ROLE_ADMIN 账户:用户名 admin,密码 123
    ROLE_USER 账户:用户名 jitwxs,密码 123

    运行结果

    注:如果你想要将密码加密,可以修改 configure() 方法如下:

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         auth.userDetailsService(userDetailsService)
             .passwordEncoder(new BCryptPasswordEncoder());
     }
    
    展开全文
  • Spring Security 工作原理概览

    万次阅读 多人点赞 2019-04-28 01:43:59
    本文由读者 muggle 投稿,muggle 是一位具备极客精神的90后单身老实猿,对 Spring Security 有丰富的使用经验,muggle 个人博客地址是 h...
        

    本文由读者 muggle 投稿,muggle 是一位具备极客精神的90后单身老实猿,对 Spring Security 有丰富的使用经验,muggle 个人博客地址是 https://muggle0.github.io。

    Security 原理分析

    SpringSecurity 过滤器链

    SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤器链的各个进行说明:

    1. WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。

    2. SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的。

    3. HeaderWriterFilter:用于将头信息加入响应中。

    4. CsrfFilter:用于处理跨站请求伪造。

    5. LogoutFilter:用于处理退出登录。

    6. UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。

    7. DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。

    8. BasicAuthenticationFilter:检测和处理 http basic 认证。

    9. RequestCacheAwareFilter:用来处理请求的缓存。

    10. SecurityContextHolderAwareRequestFilter:主要是包装请求对象request。

    11. AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。

    12. SessionManagementFilter:管理 session 的过滤器

    13. ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。

    14. FilterSecurityInterceptor:可以看做过滤器链的出口。

    15. RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。

    SpringSecurity 流程图

    先来看下面一个 Spring Security 执行流程图,只要把 SpringSecurity 的执行过程弄明白了,这个框架就会变得很简单:

    640?wx_fmt=png

    流程说明

    1. 客户端发起一个请求,进入 Security 过滤器链。

    2. 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。

    3. 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。

    4. 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层否则到 AccessDeniedHandler 鉴权失败处理器处理。

    Security 配置

    WebSecurityConfigurerAdapter 这个类里面可以完成上述流程图的所有配置

    配置类伪代码

    @Configuration	
    @EnableWebSecurity	
    public class SecurityConfig extends WebSecurityConfigurerAdapter {	
        @Override	
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {	
            auth.userDetailsService(userDetailService).passwordEncoder(new BCryptPasswordEncoder());	
        }	
        @Override	
        public void configure(WebSecurity web) throws Exception {	
            web.ignoring().antMatchers("/resources/**/*.html", "/resources/**/*.js");	
        }	
        @Override	
        protected void configure(HttpSecurity http) throws Exception {	
           http	
           .formLogin()	
           .loginPage("/login_page")	
           .passwordParameter("username")	
           .passwordParameter("password")	
           .loginProcessingUrl("/sign_in")	
           .permitAll()	
           .and().authorizeRequests().antMatchers("/test").hasRole("test")	
           .anyRequest().authenticated().accessDecisionManager(accessDecisionManager())	
           .and().logout().logoutSuccessHandler(new MyLogoutSuccessHandler())	
           .and().csrf().disable();	
           http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);	
           http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler());	
           http.addFilterAfter(new MyFittler(), LogoutFilter.class);	
        }	
    }

    配置简介

    • configure(AuthenticationManagerBuilder auth)

    AuthenticationManager 的建造器,配置 AuthenticationManagerBuilder 会让Security 自动构建一个 AuthenticationManager(该类的功能参考流程图);如果想要使用该功能你需要配置一个 UserDetailService 和 PasswordEncoder。UserDetailsService 用于在认证器中根据用户传过来的用户名查找一个用户, PasswordEncoder 用于密码的加密与比对,我们存储用户密码的时候用PasswordEncoder.encode() 加密存储,在认证器里会调用 PasswordEncoder.matches() 方法进行密码比对。如果重写了该方法,Security 会启用 DaoAuthenticationProvider 这个认证器,该认证就是先调用 UserDetailsService.loadUserByUsername 然后使用 PasswordEncoder.matches() 进行密码比对,如果认证成功成功则返回一个 Authentication 对象。

    • configure(WebSecurity web)

    这个配置方法用于配置静态资源的处理方式,可使用 Ant 匹配规则。

    • configure(HttpSecurity http)

    这个配置方法是最关键的方法,也是最复杂的方法。我们慢慢掰开来说:

    http	
    .formLogin()	
    .loginPage("/login_page")	
    .passwordParameter("username")	
    .passwordParameter("password")	
    .loginProcessingUrl("/sign_in")	
    .permitAll()

    这是配置登录相关的操作从方法名可知,配置了登录页请求路径,密码属性名,用户名属性名,和登录请求路径,permitAll()代表任意用户可访问。

    http	
    .authorizeRequests()	
    .antMatchers("/test").hasRole("test")	
    .anyRequest().authenticated()	
    .accessDecisionManager(accessDecisionManager());

    以上配置是权限相关的配置,配置了一个 /test url 该有什么权限才能访问, anyRequest() 表示所有请求,authenticated() 表示已登录用户才能访问, accessDecisionManager() 表示绑定在 url 上的鉴权管理器

    为了对比,现在贴出另一个权限配置清单:

    http.authorizeRequests()	
    .antMatchers("/tets_a/**","/test_b/**").hasRole("test")	
    .antMatchers("/a/**","/b/**").authenticated()	
    .accessDecisionManager(accessDecisionManager())

    我们可以看到权限配置的自由度很高,鉴权管理器可以绑定到任意 url 上;而且可以硬编码各种 url 权限:

    http	
    .logout()	
    .logoutUrl("/logout")	
    .logoutSuccessHandler(new MyLogoutSuccessHandler())

    登出相关配置,这里配置了登出 url 和登出成功处理器:

    http	
    .exceptionHandling()	
    .accessDeniedHandler(new MyAccessDeniedHandler());

    上面代码是配置鉴权失败的处理器。

    http.addFilterAfter(new MyFittler(), LogoutFilter.class);	
    http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);

    上面代码展示如何在过滤器链中插入自己的过滤器,addFilterBefore 加在对应的过滤器之前,addFilterAfter 加在对应的过滤器之后,addFilterAt 加在过滤器同一位置,事实上框架原有的 Filter 在启动 HttpSecurity 配置的过程中,都由框架完成了其一定程度上固定的配置,是不允许更改替换的。根据测试结果来看,调用 addFilterAt 方法插入的 Filter ,会在这个位置上的原有 Filter 之前执行。

    注:关于 HttpSecurity 使用的是链式编程,其中 http.xxxx.and.yyyyy 这种写法和 http.xxxx;http.yyyy 写法意义一样。

    • 自定义 AuthenticationManager 和 AccessDecisionManager

    重写 authenticationManagerBean() 方法,并构造一个 authenticationManager:

    @Override	
    public AuthenticationManager authenticationManagerBean() throws Exception {	
        ProviderManager authenticationManager = new ProviderManager(Arrays.asLis(getMyAuthenticationProvider(),daoAuthenticationProvider()));	
        return authenticationManager;	
    }

    我这里给 authenticationManager 配置了两个认证器,执行过程参考流程图。

    定义构造AccessDecisionManager的方法并在配置类中调用,配置参考 configure(HttpSecurity http) 说明:

    public AccessDecisionManager accessDecisionManager(){	
        List<AccessDecisionVoter<? extends Object>> decisionVoters	
                = Arrays.asList(	
                new MyExpressionVoter(),	
                new WebExpressionVoter(),	
                new RoleVoter(),	
                new AuthenticatedVoter());	
        return new UnanimousBased(decisionVoters);	
    }

    投票管理器会收集投票器投票结果做统计,最终结果大于等于0代表通过;每个投票器会返回三个结果:-1(反对),0(通过),1(赞成)。

    Security 权限系统

    • UserDetails

    Security 中的用户接口,我们自定义用户类要实现该接口。

    • GrantedAuthority

    Security 中的用户权限接口,自定义权限需要实现该接口:

    public class MyGrantedAuthority implements GrantedAuthority {	
        private String authority;	
    }

    authority 表示权限字段,需要注意的是在 config 中配置的权限会被加上 ROLE_ 前缀,比如我们的配置 authorizeRequests().antMatchers("/test").hasRole("test"),配置了一个 test 权限但我们存储的权限字段(authority)应该是 ROLE_test

    • UserDetailsService

    Security 中的用户 Service,自定义用户服务类需要实现该接口:

    @Service	
    public class MyUserDetailService implements UserDetailsService {	
        @Override	
        public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {	
          return.....	
        }	
    }

    loadUserByUsername的作用在上文中已经说明,就是根据用户名查询用户对象。

    • SecurityContextHolder

    用户在完成登录后 Security 会将用户信息存储到这个类中,之后其他流程需要得到用户信息时都是从这个类中获得,用户信息被封装成 SecurityContext ,而实际存储的类是 SecurityContextHolderStrategy ,默认的SecurityContextHolderStrategy 实现类是 ThreadLocalSecurityContextHolderStrategy 它使用了ThreadLocal来存储了用户信息。

    手动填充 SecurityContextHolder 示例:

    UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("test","test",list);	
    SecurityContextHolder.getContext().setAuthentication(token);

    对于使用 token 鉴权的系统,我们就可以验证token后手动填充SecurityContextHolder,填充时机只要在执行投票器之前即可,或者干脆可以在投票器中填充,然后在登出操作中清空SecurityContextHolder。

    Security 扩展

    Security 可扩展的有

    1. 鉴权失败处理器

    2. 验证器

    3. 登录成功处理器

    4. 投票器

    5. 自定义token处理过滤器

    6. 登出成功处理器

    7. 登录失败处理器

    8. 自定义 UsernamePasswordAuthenticationFilter

    • 鉴权失败处理器

    Security 鉴权失败默认跳转登录页面,我们可以实现 AccessDeniedHandler 接口,重写 handle() 方法来自定义处理逻辑;然后参考配置类说明将处理器加入到配置当中。

    • 验证器

    实现 AuthenticationProvider 接口来实现自己验证逻辑。需要注意的是在这个类里面就算你抛出异常,也不会中断验证流程,而是算你验证失败,我们由流程图知道,只要有一个验证器验证成功,就算验证成功,所以你需要留意这一点。

    • 登录成功处理器

    在 Security 中验证成功默认跳转到上一次请求页面或者路径为 "/" 的页面,我们同样可以自定义:继承 SimpleUrlAuthenticationSuccessHandler 这个类或者实现 AuthenticationSuccessHandler 接口。我这里建议采用继承的方式,SimpleUrlAuthenticationSuccessHandler 是默认的处理器,采用继承可以契合里氏替换原则,提高代码的复用性和避免不必要的错误。

    • 投票器

    投票器可继承 WebExpressionVoter 或者实现 AccessDecisionVoter接口;WebExpressionVoter 是 Security 默认的投票器;我这里同样建议采用继承的方式;添加到配置的方式参考 上文;

    注意:投票器 vote 方法返回一个int值;-1代表反对,0代表弃权,1代表赞成;投票管理器收集投票结果,如果最终结果大于等于0则放行该请求。

    • 自定义token处理过滤器

    自定义 token 处理器继承自 OncePerRequestFilter 或者 GenericFilterBean 或者 Filter 都可以,在这个处理器里面需要完成的逻辑是:获取请求里的 token,验证 token 是否合法然后填充 SecurityContextHolder ,虽然说过滤器只要添加在投票器之前就可以,但我这里还是建议添加在 http.addFilterAfter(new MyFittler(), LogoutFilter.class);

    • 登出成功处理器

    实现LogoutSuccessHandler接口,添加到配置的方式参考上文。

    • 登录失败处理器

    登录失败默认跳转到登录页,我们同样可以自定义。继承 SimpleUrlAuthenticationFailureHandler 或者实现 AuthenticationFailureHandler,建议采用继承。

    • 自定义UsernamePasswordAuthenticationFilter

    我们自定义UsernamePasswordAuthenticationFilter可以极大提高我们 Security的灵活性(比如添加验证验证码是否正确的功能)。

    我们直接继承 UsernamePasswordAuthenticationFilter ,然后在配置类中初始化这个过滤器,给这个过滤器添加登录失败处理器,登录成功处理器,登录管理器,登录请求 url 。

    这里配置略微复杂,贴一下代码清单

    初始化过滤器:

    MyUsernamePasswordAuthenticationFilte getAuthenticationFilter(){	
        MyUsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter = new MyUsernamePasswordAuthenticationFilter(redisService);	
        myUsernamePasswordAuthenticationFilter.setAuthenticationFailureHandler(new MyUrlAuthenticationFailureHandler());	
        myUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());	
        myUsernamePasswordAuthenticationFilter.setFilterProcessesUrl("/sign_in");	
        myUsernamePasswordAuthenticationFilter.setAuthenticationManager(getAuthenticationManager());	
        return myUsernamePasswordAuthenticationFilter;	
    }

    添加到配置:

    http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);

    总结

    对于 Security 的扩展配置关键在于 configure(HttpSecurityhttp) 方法;扩展认证方式可以自定义 authenticationManager 并加入自己验证器,在验证器中抛出异常不会终止验证流程;扩展鉴权方式可以自定义 accessDecisionManager 然后添加自己的投票器并绑定到对应的 url(url 匹配方式为 ant)上,投票器 vote(Authenticationauthentication,FilterInvocationfi,Collection<ConfigAttribute>attributes) 方法返回值为三种:-1 0 1,分别表示反对弃权赞成。

    对于 token 认证的校验方式,可以暴露一个获取的接口,或者重写 UsernamePasswordAuthenticationFilter 过滤器和扩展登录成功处理器来获取 token,然后在 LogoutFilter 之后添加一个自定义过滤器,用于校验和填充 SecurityContextHolder。

    另外,Security 的处理器大部分都是重定向的,我们的项目如果是前后端分离的话,我们希望无论什么情况都返回 json ,那么就需要重写各个处理器了。


    640640关注牧码小子,后台回复 Java ,领取松哥为你精心准备的Java干货!640 

    往期文章一览

    1、工作之余,你是怎么提高技术的?

    2、两年了,我写了这些干货!

    3、想和大家谈一点合作

    4、一个Java程序猿眼中的前后端分离以及Vue.js入门

    5、跟着平台混了四年,现在要单飞了!

    640?wx_fmt=png你点的每个在看,我都认真当成了喜欢


    展开全文
  • dubbo,redis,solr,freemarker,activeMQ,springBoot框架,微信支付,nginx负载均衡,电商活动秒杀,springSecurity安全框架,FastDFS分布式文件服务器,还会涉及到代码生成器,   前台的技术有angularJS和...
  • 说起考security+这个认证考试还是比较尴尬的,因为根本没想过要考这个认证,但是现在想混安全界必须有几本证在手,没有证书很难找到高薪资的工作。而且相比CISP、CISSP而言,Security+的性价比还是很高的。很值得...
  • Security+认证考试心得分享

    千次阅读 2018-09-29 17:51:03
    前不久刚考了security+,也算是幸运,低分飘过,不敢说有什么经验,只是在这里跟大家分享一下我的一些心得吧。 1、此处非广告,因为自己对于security+前期没什么了解,只是从朋友那里听说这是个关于安全的认证考试,...
  • security

    2019-07-18 16:42:54
    spring security + Oauth2.0 一.spring security 问: spring security是如何注入spring容器? filter是如何加入tomcat? spring security是如何起作用? 1.自动注入 springboot在启动时会扫描所有jar包下的spring....
  • Security+考试经验分享

    2019-10-14 16:25:54
    Security+考试经验分享 X00 前言 报名Security+考试的原因主要是因为在安全圈子工作了几年,发现欠缺的知识还是比较多的,个人工作偏售前一些,没有强大的行业知识背景和安全知识体系这些作为支撑的话,心里会...
  • SpringBoot基于SpringSecurity表单登录和权限验证

    万次阅读 多人点赞 2019-01-11 16:16:11
    上篇介绍了一个自己做的管理系统,最近空闲的时间自己在继续做,把之前登录时候自定义的拦截器过滤器换成了基于SpringSecurity来做,其中遇到了很多坑,总结下,大家有遇到类似问题的话就当是为大家闭坑吧。...
  • 一、 前言:项目舍弃了原本的SSH框架,改用Spring Boot框架,并且要引入Spring Security为系统提供安全访问控制解决方案,接下来记录一下这两天在Spring Boot中引入Spring Security 的过程。主要参考了以下项目、...
  • 基于注解的Spring Security原理解析

    万次阅读 2017-12-07 11:05:15
    概述:Spring Security就是引入了一系列的SecurityFilter,将其添加到Spring中去了;在有请求时,根据URL是否符合每个Filter的规则来判断是否需要该Filter来进行处理。
  • 本博文主要讲述cre/cred/crim/crit/cruc/cult/cub/cur/curr/cuss的由来及词源知识C的故事及部分D词根
  • SpringBoot security关闭验证yml配置

    万次阅读 2018-09-28 11:42:40
    #监控关闭 security: basic: enabled: false management: security: enabled: false
  • security.basic.enabled 配置过时或不可用

    万次阅读 2018-05-24 16:55:55
    本人spring boot 版本是:2.1.0.BUILD-SNAPSHOT security 版本是:5.05 在 配置文件中 security.basic.enabled 等一系列都提示过时,查看官方文档默认情况...也可以通过如下属性配置: spring.security.user.na...
  • springboot2.0+,整合security配置security关闭http基本验证

    万次阅读 多人点赞 2019-01-12 15:14:34
    今天学习security,遇到了很多坑例如: spring boot1.5配置security关闭http基本验证,只需要在application.properites中配置 security.basic.enabled=false即可, 但是spring boot 2.0+之后这样配置就不能生效了。 ...
  • NetworkSecurityConfig: No Network Security Config specified, using platform default 出现这个问题,一直不能解决 已经加了配置 uses-permission android:name="android.permission.INTERNET" /> uses-...
  • 注册表修改方法 计算机\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\wscsvc 里面start由4(禁用)改为2(自动)然后重启电脑
  • 1.修改JDK的配置文件:%JDK_HOME%\jre\lib\security\java.security  //加上  security.provider.10=org.bouncycastle.jce.provider.BouncyCastleProvider    2.在代码里直接添加  Java代码  ...
1 2 3 4 5 ... 20
收藏数 706,225
精华内容 282,490
关键字:

security