Spring Security简介

背景分析

企业中数据是最重要的资源,对于这些数据而言,有些可以直接匿名访问,而有些只能登录以后才能访问,也有一些,你登录成功之后还是不能够访问,因为你没有权限也不能访问。总之这些规则都是保护系统资源不被破坏的一种手段,几乎每个系统中都需要这样的措施对数据(资源)进行保护,我们通常会通过这些软件技术对这样的业务进行具体的设计和实现。早期没有统一的标准,每个系统都有自己的独立的设计实现,但是对于这个业务又是一个共性,后续市场上就基于共享做了具体的落地实现,例如:Spring Security、Apache Shiro的诞生。

Spring Security 概述

Spring Security是一个企业级安全框架,由Spring官方推出,它对软件系统中的认证、授权、加密、防护等功能进行了封装,并在SpringBoot技术推出之后,在配置方面做了很大的简化,市场上现在的分布式架构下的安全控制正在逐步的转向Spring Security。

Spring Security 基本架构

Spring Security在企业中实现认证和授权业务的同时,底层构建了大量的过滤器。

springsecurity使用SpringSession_html


其中:

绿色部分为认证过滤器,需要我们自己配置,也可以配置个人认证过滤器.也可以使用Spring Security提供的默认认证过滤器.黄色部分为授权过滤器.Spring Security就是通过这些过滤器调用相关对象一起完成认证和授权操作。

Spring Security 快速入门

创建工程

springsecurity使用SpringSession_spring_02

添加项目依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.3.2.RELEASE</version>
    </parent>

    <groupId>com.cy</groupId>
    <artifactId>02-jt-spring-security</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
</project>

创建配置文件

在resources目录下创建application.yml文件,并指定服务端口:

server:
   port: 8181

创建项目启动类

package com.cy.jt;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

运行启动类访问测试

第一步:检查控制输出,是否自动生成了一个密码,例如:

Using generated security password: 360123aa-df93-4cd9-bab4-5212af421d2c

第二步:打开浏览器输入http://localhost:8181然后呈现登录页面,例如:

springsecurity使用SpringSession_html_03


在登录窗口中输入用户名(系统默认 user),密码(服务启动时,会在控制台默认输出密码),然后点击Sign in进行登录,登录成功后会默认出现如下界面:

springsecurity使用SpringSession_maven_04

定义登录成功页面

在项目的resources目录下创建static目录,并在此目录创建一个index.html文件。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<h1>登录成功</h1>
</body>
</html>

创建完成后,再次启动服务进行测试,登录成功之后,系统会默认跳转到index.html页面,例如:

springsecurity使用SpringSession_maven_05

配置登录信息(用户名和密码)

第一步:编写一个方法(可在启动类中调用执行),对一个明文进行加密,例如:

package com.cy.jt;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@SpringBootApplication
public class SpringSecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run( SpringSecurityApplication.class,args);
       encodePwd();
    }
 //通过Spring Security中的BCryptPasswordEncoder来对密码进行加密(Bcrypt底层基于随机盐方式对密码进行hash不可逆加密)
    static void encodePwd(){
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();//对密码进行加密的对象
        String pwd=encoder.encode("xq123456");
        System.out.println(pwd);
    }
}

第二步:将用户名和密码在application.yml文件中进行配置,例如:

server:
  port: 8181
spring:
  security:
    user:
      name: xiongqiang
     # password: xq123456 //密码过于简单
      password: '{bcrypt}$2a$10$dShXkkcPRsJrcbdu6fDdxuqIh6f6LDELHBj.0Oh/8Y90qLR9ft8oy'

其中,{bcrypt}指定了密码加密时使用的算法。
第三步:重启服务,进行登录测试。

Spring Security 认证逻辑实践

认证授权分析

用户在进行资源访问时,要求系统要对用户进行权限控制,其具体流程如图所示:

springsecurity使用SpringSession_html_06

自定义登录逻辑

Spring Security支持通过配置文件的方式定义用户信息(账号密码和角色等),但是这种方式有明显的缺点,那就是系统上线之后,用户信息的变更比较麻烦。因此SpringSecurity还支持通过实现UserDetailsService接口的方式来提供用户认证授权信息,其应用过程如下:
第一步:定义security配置类,例如:

@Configuration
/*此注解表示描述的类是spring中的配置类,配置类会在spring工程启动时优先加载,在配置类中通常会
对第三方资源进行初始配置。
*/
public class SecurityConfig {
    /**
     * @Bean此注解通常会在@Configuration注解描述的类中描述方法,用于告诉spring框架这个方法的返回值
     * 会交给spring管理,并给spring管理的这个对象起个默认的名字,这个名字与方法名相同,当然也可以
     * 通过@Bean注解自定义名字。
     */
    @Bean
    //@Bean("brcytPassword") //自定义bean对象名字为:brcytPassword
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    }

第二步:定义UserDetailService接口实现类,自定义登录逻辑,代码如下:
UserDetailService为SpringSecurity官方提供了登录逻辑处理对象,我们自己可以实现此接口,然后在对应的方法中进行登录逻辑的编写即可

@Service
public class UserDetailServiceImpl implements UserDetailsService {
    /**
     * @Autowired注解描述属性时的规则:
     * spring框架会依据@Autowired注解描述的属性类型,从spring容器查找对应的Bean,假如只找到一个则直接注入,假如找到多个
     * 还会对比属性名是否与容器中的Bean的名字是否相同,有则直接注入,没有则抛出异常。
     */
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    /**
     * 当我们执行登录操作时,底层会通过过滤器等对象,调用这个方法.
     * @param username 此参数为页面输出的用户名
     * @return 一般是从数据库基于用户名查询到的用户信息
     * @throws UsernameNotFoundException
     */

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //1.基于用户名从数据库查询用户信息
        //User user = userMapper.selectUserByUsername(username);
        if(!"xiongqiang".equals(username))throw new UsernameNotFoundException("用户不存在");
        //2.将用户信息封装到UserDetails对象中并返回
        //假设这个密码是从数据库中查询到的
        String encodedPwd = passwordEncoder.encode("123456");
        //假设这个权限信息是从数据库查询到的
        //假设分配权限的方式是角色,编写字符串时用的“ROLE_"做前缀,以下授予了搜索和新建的权限
        List<GrantedAuthority> grantedAuthorities =
                AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin,ROLE_normal,sys:res:retrieve,sys:res:create");


        //这个user是SpringSecurity提供的UserDetails接口的实现,用于封装用户信息
        User user = new User(username, encodedPwd,grantedAuthorities);
        return user;
    }
}

说明:这里的User对象会交给 SpringSecurity框架,框架提取出密码信息,然后与用户输入的密码进行匹配校验。
第三步:启动服务进行登录,访问测试。

自定义登录界面

第一步:定义登录页面(直接在static目录下创建即可),关键代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录界面</title>
</head>
<body>
<h2>Please Login</h2>
<form action="/login" method="post">
    <ul>
        <li>username:</li>
        <li><input type="text" name="username"></li>
        <li>password:</li>
        <li><input type="password" name="password"></li>
        <li><input type="submit" value="登录"></li>
    </ul>
</form>
</body>
</html>

注意:请求的url暂时为“/login”,请求方式必须为post方式,请求的参数暂时必须为username,password。这些规则默认在UsernamePassWordAuthenticationFiler中进行定义。
第二步:修改安全配置类,让其实现接口,并重写相关的方法,进行登录设计,代码如下:

@Configuration
public class SecutiryConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //super.configure(http);
        //关闭跨域攻击,不关闭容易出错
        http.csrf().disable();
        //自定义登陆表单
        http.formLogin()
                //设置登陆页面
                .loginPage("/login.html")
                //设置登陆请求处理地址(对应form表单中的action),登陆时会访问UserDetailService对象
                .loginProcessingUrl("/login")
                //设置请求用户名参数为username(默认就是username,可以自己修改,需要与表单同步)
                .usernameParameter("username")
                //请求请求密码参数为password(默认就是password,可以自己修改,需要与表单同步)
                .passwordParameter("password")
                //设置登陆成功跳转页面(默认为/index.html)
                .defaultSuccessUrl("/index.html")
                //登陆失败访问的页面(默认为/login.html?error)
                .failureUrl("/login.html?error");
        //认证设计
        http.authorizeRequests()
                //设置要放行的资源
                .antMatchers("/login.html").permitAll() //可直接访问
                //设置需要认证的请求(除了上面的要放行,其它都要进行认证)
                .anyRequest().authenticated();
    }
}

登录成功和失败处理器

现在的很多系统都采用的是前后端分离设计,我们登录成功以后可能会跳转到前端系统的某个地址,或者是返回一个json数据,我们可以自己定义登录成功的处理操作,例如:
第一步:定义登录成功处理器,关键代码如下:

class RedirectAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private String redirectUrl;//定义要跳转的url
    public RedirectAuthenticationSuccessHandler(String redirectUrl){
        this.redirectUrl = redirectUrl;
    }
    /*登录成功以后,会执行此方法 */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,
                                        HttpServletResponse httpServletResponse,
                                        Authentication authentication) throws IOException, ServletException {
    httpServletResponse.sendRedirect(redirectUrl);
    }
}

第二步:定义登录失败处理器,可仿照成功处理器来实现。
第三步:修改配置类,设置登录成功与失败处理器。

@Configuration
public class SecutiryConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //super.configure(http);
        //关闭跨域攻击,不关闭容易出错
        http.csrf().disable();
        //自定义登陆表单
        http.formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/login")              
                .usernameParameter("username")               
                .passwordParameter("password")
                //设置登陆成功跳转页面(默认为/index.html)
                .successHandler(new RedirectAuthenticationSuccessHandler("跳转url"))
                //登陆失败访问的页面(默认为/login.html?error)
               .failureHandler(new RedirectAuthenticationFailureHandler("跳转url"))
        //认证设计
                http.authorizeRequests()
                //设置要放行的资源
                .antMatchers("/login.html").permitAll()
                //设置需要认证的请求(除了上面的要放行,其它都要进行认证)
               .anyRequest().authenticated();
    }
}

第四步:启动服务进行测试。

放行静态资源

在SecurityManager配置类中的configure(HttpSecurity http)方法中我们可以通过对antMatchers方法来定义需要放行的静态资源,例如:

http.authorizeRequests()
.authorizeRequests() //设置请求的授权
        .antMatchers(   //配置下列路径的授权
                "/index.html",
                "/js/*",
                "/css/*",
                "/img/**",
                "/bower_components/**",
                "/login.html"
        ).permitAll()   //设置上述所有路径不需要登录就能访问(放行)

其中:
(1)“*”用于匹配0个或多个字符。
(2)“**”用于匹配0个或多个目录及字符

Spring Security 授权逻辑实践

修改授权配置类

在权限配置类上添加启用全局方法访问控制的注解,例如

@EnableGlobalMethodSecurity(prePostEnabled = true)

此注解由SpringSecurity提供,用于描述权限配置类,告诉系统底层在启动时,进行访问权限的初始化。
prePostEnabled=true表示启动权限管理功能。

定义Controller资源

定义一个ResourceController类,作为资源访问对象,例如:

/**
 * 可以将这里的Controller看成系统内部的一个资源对象,我们要求访问此对象中的方法时需要进行权限检查。
 */
@RestController
public class ResourceController {
    //@PreAuthorize注解用于描述访问方法时首先要对用户是否有访问该方法的权限进行检验

    //1.添加操作
    //@PreAuthorize("hasRole('admin')")//登录用户具备admin这个角色才可以访问
    @PreAuthorize("hasAnyAuthority('sys:res:create')") //登录用户具备sys:res:create权限才能访问资源
    @RequestMapping("/doCreate")
    public String doCreate(){
        return "insert resource  data Ok!";
    }
    //2.查询操作
    @PreAuthorize("hasAnyAuthority('sys:res:retrieve')")
    @RequestMapping("/doRetrieve")
    public String doRetrieve(){
        return "select resource  data Ok!";
    }
    //3.更新操作
    @PreAuthorize("hasAnyAuthority('sys:res:update')")
    @RequestMapping("/doUpdate")
    public String doUpdate(){
        return "update resource data Ok!";
    }

    //4.删除操作
    @PreAuthorize("hasAnyAuthority('sys:res:delete')")
    @RequestMapping("/doDelete")
    public String doDelete(){
        return  "delete resource data Ok!";
    }
}

其中,@PreAuthorize注解描述方法时,用于告诉系统访问此方法时需要进行权限检测。该用户需要具备指定的权限才可以进行访问。
例如:

  • @PreAuthorize(“hasAuthority(‘sys:res:delete’)”) 设定需要具备sys:res:delete权限才能访问。
  • @PreAuthorize(“hasRole(‘admin’)”) 设定需要具备admin角色才能访问。

启动服务访问测试

使用不同用户进行登录,然后执行资源访问,假如没有权限,则会看到响应状态为403。

SpringSecurity认证与授权异常处理

异常类型

对于SpringSecurity框架而言,在实现认证和授权业务时,可能会出现以下两大类型异常:
(1)AuthenticationException:用户还未认证就去访问需要认证之后才能访问的方法时会出现这个异常,通常对应的状态码为401;
(2)AccessDeniedException:用户认证之后,在访问一些没有权限的资源时,可能会出现这个异常,通常对应的状态码为403;

异常处理规范

SpringSecurity框架给了默认的异常处理方式,当默认的异常处理方式不满足我们实际业务需求时,此时我们就要自己定义异常处理逻辑,编写逻辑时需要遵循如下规范:
(1)AuthenticationEntryPoint:统一处理AuthenticationException异常。
(2)AccessDeniedHandler:统一处理AccessDeniedException异常。

自定义异常处理对象

处理没有认证的访问异常

public class DefaultAuthenticationEntryPoint implements AuthenticationEntryPoint {

   @Override
   public void commence(HttpServletRequest httpServletRequest,
                        HttpServletResponse httpServletResponse,
                        AuthenticationException e) throws IOException, ServletException {
       //方案1:重定向
       // httpServletResponse.sendRedirect("http://www.tedu.cn");

       //方案2:假如访问被拒绝了,则向客户端响应一个json格式的字符串
       httpServletResponse.setCharacterEncoding("utf-8");
       httpServletResponse.setContentType("application/json;charset=utf-8");
       PrintWriter out = httpServletResponse.getWriter();
       Map<String,Object> map = new HashMap<>();
       map.put("state", 401);
       map.put("message", "请先登录!");

       String js = new ObjectMapper().writeValueAsString(map);
       out.println(js);
       out.flush();
       out.close();

   }
}

处理没有权限时抛出的异常

public class DefaultAccessDeniedExceptionHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest,
                       HttpServletResponse httpServletResponse,
                       AccessDeniedException e) throws IOException, ServletException {
        //方案1:重定向
       // httpServletResponse.sendRedirect("http://www.tedu.cn");

        //方案2:假如访问被拒绝了,则向客户端响应一个json格式的字符串
        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.setContentType("application/json;charset=utf-8");
        PrintWriter out = httpServletResponse.getWriter();
        Map<String,Object> map = new HashMap<>();
        map.put("state", 403);
        map.put("message", "没有访问权限,请联系管理员!");

        String js = new ObjectMapper().writeValueAsString(map);
        out.println(js);
        out.flush();
        out.close();
    }
}

配置异常处理对象

在配置类SecurityConfig中添加自定义处理对象,代码如下:

//设置需要认证与拒绝访问的异常处理器
      http.exceptionHandling()
              .authenticationEntryPoint(new DefaultAuthenticationEntryPoint())//未认证时
              .accessDeniedHandler(new DefaultAccessDeniedExceptionHandler());//未授权时

会话状态分析与实践

会话状态

客户端与服务端通讯过程中产生的状态信息(类似会议记录),称之为会话状态。

会话状态的存储

客户端浏览器与服务端通讯时使用的是http协议,这个协议本身就是无状态协议,也就是说通过此协议,无法存储会话状态,此时在服务端与客户端就采用一种Cookie。

会话技术分析

Cookie技术

Cookie是由服务端创建,并且是在客户端存储会话状态的对象,此对象分为两种类型,一种为会话Cookie,一种为持久Cookie,浏览器在访问具体的某个域名时会携带这个域的有效Cookie到服务端。
(1)会话Cookie:浏览器关闭Cookie生命周期也就结束了(一般默认都是会话Cookie)
(2)持久Cookie:持久Cookie是在Cookie对象创建时指定了生命周期,例如一周时间,即使关闭了浏览器,但持久Cookie依旧有效。

Session技术

Session技术由服务端创建,并且是在服务端存储会话状态的对象,当Session对象创建时,还会创建一个会话Cookie对象,并且通过这个会话Cookie将SessionID写到客户端,客户端下次访问服务端会携带这个会话Cookie,并且通过JsessionID找到Session对象,进而获取Session对象中存储的数据,Cookie默认的生命周期为30分钟。

在SpringSecurity中获取用户认证用户信息,代码如下:

public String doGetUser(){
        //从Session中获取用户认证信息
        //1)Authentication 认证对象(封装了登录用户信息的对象)
        //2)SecurityContextHolder 持有登录状态的信息的对象(底层可通过session获取用户信息)
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        //基于认证对象获取用户身份信息
        User principal =  (User) authentication.getPrincipal();//获取资源
        System.out.println("principal.class:"+principal.getClass());
        return principal.getUsername()+":"+principal.getAuthorities();
    }

无状态的会话技术分析

有状态的会话实现在分布式架构中可能会存在很多问题,例如,浏览器默认不支持携带其他区域的cookie信息进行资源访问,同时服务端的Session默认不能共享。当我们有一种方式可以将session持久化到一些数据库,例如:Redis,下次请求到其他服务器时,可以直接从redis中获取登录信息,但是假如并发比较大,数据库的访问压力就会剧增,所以现在还有一种方案就是将用户的登录状态信息都存储到客户端,服务端不记录任何状态,服务端只负责对客户端传递过来的状态信息进行解析,基于此方式进行用户登录状态判断,这样的会话过程称为无状态会话。