Spring Security

背景分析

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

认证授权分析

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

Spring Security概述

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

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

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

Spring Security入门

创建工程

添加项目依赖

<?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>

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

    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.3.2.RELEASE</version>
    </parent>

    <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: 8080

创建项目启动类

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);
    }
}

运行启动类

第一步:检查控制台输出。会自动生成一个密码,例如:

 第二步:打开浏览器输入http://localhosh:8080,然后会呈现登录界面,例如;

 在登陆窗口中输入用户名user(系统默认),密码(服务器启动时,控制台默认输出的密码,也就是第一步生成的密码)然后登陆,默认会出现如下界面:

 定义登录成功页面

在resources目录下创建static目录,并在此目录下创建一个index.html文件例如:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
  <h1>Login Ok</h1>
</body>
</html>

重启服务,再次进行登录访问测试,登陆成功以后系统默认会跳转到index.html页面

配置登录密码

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

 static void encodePwd(){
        BCryptPasswordEncoder encoder=new BCryptPasswordEncoder();
        String password="123456";//明文
        String newPwd=encoder.encode("123456");
        System.out.println(newPwd);//$2a$10$fahHJIe3SJm3KcyiPPQ2d.a2qR029gB3qKHrKanQ87u.KbtZ6Phr.
    }

第二步:将用户和密码在springboot工程的application.yml文件中进行配置:

spring:
  security:
    user:
      name: jack
      #password: 123456 #这种写法,密码太简单了
      password: '{bcrypt}$2a$10$fahHJIe3SJm3KcyiPPQ2d.a2qR029gB3qKHrKanQ87u.KbtZ6Phr.'

{bcrypt}指定了密码加密时使用的算法

第三步:重启服务,重新进行登录测试

Spring Security认证逻辑实现

自定义登录逻辑

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

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

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

package com.cy.jt.security.service;
@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;
    /**
     * 当我们执行登录操作时,底层会通过过滤器等对象,调用这个方法.
     * @param username 这个参数为页面输出的用户名
     * @return 一般是从数据库基于用户名查询到的用户信息
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        //1.基于用户名从数据库查询用户信息
        if(!"jack".equals(username))//假设这是从数据库查询的信息
            throw new UsernameNotFoundException("user not exists");
        //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接口的实现,用于封装用户信息
        //后续我们也可以基于需要自己构建UserDetails接口的实现
        User user=new User(username,encodedPwd,grantedAuthorities);
        return user;
    }
}

说明,这里的User对象会交给SpringSecurity框架,框架提取出密码信息,然后与用户输入的密码进行匹配校验.

第三步:启动服务进行登陆,访问测试。

自定义登录界面

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

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--<img src="/images/red.png"/>-->
<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="Sign in"></li>
        </ul>
    </form>
</body>
</html>

注意:请求的url暂时为”/login”,请求方式必须为post方式,请求的参数暂时必须为username,password。这些规则默认在UsernamePasswordAuthenticationFilter中进行了定义。

第二步:修改安全配置类,让其实现接口,并重写相关config方法,进行登陆设计,代码如下:

@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")
                //.usernameParameter("username")//设置请求用户名参数为username(默认就是username,可以自己修改,需要与表单同步)
                //.passwordParameter("password")//请求请求密码参数为password(默认就是password,可以自己修改,需要与表单同步)
                .defaultSuccessUrl("/index.html")//设置登陆成功跳转页面(默认为/index.html)
                //.successForwardUrl("/index.html");//请求转发
                .failureUrl("/login.html?error");//登陆失败访问的页面(默认为/login.html?error)
        //认证设计
        http.authorizeRequests()
                //设置要放行的咨询
                .antMatchers("/login.html").permitAll()
                //设置需要认证的请求(除了上面的要放行,其它都要进行认证)
                .anyRequest().authenticated();
    }
}

登录成功和失败处理器

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

定义登录成功处理器

方案一:可以直接执行重定向的处理器

package com.cy.jt.auth.config.authentication;
public class RedirectAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
//定义要跳转的url
    private String redirectUrl;
    public RedirectAuthenticationSuccessHandler(String redirectUrl){
        this.redirectUrl=redirectUrl;
    }
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse,
                            Authentication authentication)
            throws IOException, ServletException {
            httpServletResponse.sendRedirect(redirectUrl);
    }
}

方案2:可以直接返回JSON数据的处理器

package com.cy.jt.security.config.handler;
/**处理登录失败
 * 0)Default-默认
 * 1)Authentication-认证
 * 2)Failure-失败
 * 3)Handler-处理器
 * */
public class DefaultAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(
            HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse,
            AuthenticationException e) throws IOException, ServletException {
        //1.设置响应数据的编码
        httpServletResponse.setCharacterEncoding("utf-8");
        //2.告诉客户端响应数据的类型,以及客户端以怎样的编码进行显示
        httpServletResponse.setContentType("application/json;charset=utf-8");
        //3.获取一个输出流对象
        PrintWriter out=httpServletResponse.getWriter();
        //4.向客户端输出一个json格式字符串
        //4.1构建一个map对象
        Map<String,Object> map=new HashMap<>();
        map.put("state","500");
        map.put("msg","username or password error");
        //4.2基于jackson中的ObjectMapper对象将一个对象转换为json格式字符串
        String jsonStr= new ObjectMapper().writeValueAsString(map);
        out.println(jsonStr);
        out.flush();
    }
}

定义登录失败处理器

方案1:登陆失败重定向到页面

package com.cy.jt.auth.config.authentication;
public class RedirectAuthenticationFailureSuccessHandler implements AuthenticationFailureHandler {
    private String redirectUrl;
    public RedirectAuthenticationFailureSuccessHandler(String redirectUrl){
        this.redirectUrl=redirectUrl;
    }
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.sendRedirect(redirectUrl);
    }
}

方案2:定义登陆失败处理器

package com.cy.jt.security.config.handler;

/**处理登录失败
 * 0)Default-默认
 * 1)Authentication-认证
 * 2)Failure-失败
 * 3)Handler-处理器
 * */
public class DefaultAuthenticationFailureHandler
         implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(
            HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse,
            AuthenticationException e) throws IOException, ServletException {
        //1.设置响应数据的编码
        httpServletResponse.setCharacterEncoding("utf-8");
        //2.告诉客户端响应数据的类型,以及客户端以怎样的编码进行显示
        httpServletResponse.setContentType("application/json;charset=utf-8");
        //3.获取一个输出流对象
        PrintWriter out=httpServletResponse.getWriter();
        //4.向客户端输出一个json格式字符串
        //4.1构建一个map对象
        Map<String,Object> map=new HashMap<>();
        map.put("state","500");
        map.put("msg","username or password error");
        //4.2基于jackson中的ObjectMapper对象将一个对象转换为json格式字符串
        String jsonStr= new ObjectMapper().writeValueAsString(map);
        out.println(jsonStr);
        out.flush();
    }
}

修改配置类设置登陆成功与失败处理器

@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)
                .successHandler(new RedirectAuthenticationSuccessHandler("你的url"))
                //登陆失败访问的页面(默认为/login.html?error)
               .failureHandler(new RedirectAuthenticationFailureHandler("你的url"))
        //认证设计
                http.authorizeRequests()
                //设置要放行的咨询
                .antMatchers("/login.html").permitAll()
                //设置需要认证的请求(除了上面的要放行,其它都要进行认证)
               .anyRequest().authenticated();
    }
}

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

放行资源

在配置类中的SecurityConfig方法中可以通过antMatchers方法定义要放行的资源

      //3.放行登录url(不需要认证就可以访问)
        http.authorizeRequests()
                .antMatchers("/login.html", "/images/**")//这里写要放行的资源
                .permitAll()//允许直接访问
                .anyRequest().authenticated();//除了以上资源必须认证才可以访问

登出设计及实现

在配置类中的SecurityConfig方法中添加登出配置

  http.logout()       //开始设置登出信息
        .logoutUrl("/logout")   //登出路径
        .logoutSuccessUrl("/login.html?logout");//设置登出后显示的页面

SpringSecurity授权逻辑实现

修改授权配置类

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

package com.cy.auth.config;
//这个配置类是配置Spring-Security的,
//prePostEnabled= true表示启动权限管理功能
@EnableGlobalMethodSecurity(prePostEnabled = true)//描述权限配置类,告诉系统底层在启动时,进行访问权限的初始化配置
@Configuration
public class SpringSecurityConfigurer extends WebSecurityConfigurerAdapter {
    ……
}

定义资源Controller

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

ackage com.cy.jt.security.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 可以将这里的Controller看成是系统内部的一个资源对象,我们
 * 要求访问此对象中的方法时需要进行权限检查
 */
@RestController
public class ResourceController {
    //@PreAuthorize("hasRole('admin')")//访问方法需要权限 假如登录用户具备admin这个角色可以访问
    @PreAuthorize("hasAuthority('sys:res:create')")
    /**新增操作*/
    @RequestMapping("/doCreate")
    public String doCreate(){
        return "create resource (insert data)ok";
    }

    /**查询操作*/
    @PreAuthorize("hasAuthority('sys:res:retrieve')")//登录用户具备
    @RequestMapping("/doRetrieve")
    public String doRetrieve(){
        return "retrieve resource (retrieve data)ok";
    }
    /**修改操作*/
    @PreAuthorize("hasAuthority('sys:res:update')")//登录用户具备
    @RequestMapping("/doUpdate")
    public String doUpdate(){
        return "update resource (update data)ok";
    }

    /**删除操作*/
    @PreAuthorize("hasAuthority('sys:res:delete')")//登录用户具备
    @RequestMapping("/doDelete")
    public String doDelete(){
        return "delete resource (delete data)ok";
    }
}

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

@PreAuthorize(“hasAuthority('sys:res:delete”) 需要具备sys:res:delete权限
@PreAuthorize(“hasRole(‘admin’)”) 需要具备admin角色

启动服务访问测试

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

Spring认证和授权异常处理

异常类型

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

异常处理规范

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

自定义异常处理对象

处理没有认证的访问异常

package com.cy.jt.config;
public class DefaultAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException e) throws IOException, ServletException {
        //设置响应数据的编码
        response.setCharacterEncoding("utf-8");
        //告诉浏览器要响应的内容类型,以及编码
        response.setContentType("application/json;charset=utf-8");
        Map<String,Object> map=new HashMap<>();
        map.put("state",401);
        map.put("message","请先登录");
        PrintWriter out=response.getWriter();
        out.println(new ObjectMapper().writeValueAsString(map));
        out.flush();
        out.close();
    }
}

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

package com.cy.jt.config;

public class DefaultAccessDeniedExceptionHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException e) throws IOException, ServletException {
        //设置响应数据的编码
        response.setCharacterEncoding("utf-8");
        //告诉浏览器要响应的内容类型,以及编码
        response.setContentType("application/json;charset=utf-8");
        Map<String,Object> map=new HashMap<>();
        map.put("state",403);
        map.put("message","没有此资源的访问权限");
        PrintWriter out=response.getWriter();
        out.println(new ObjectMapper().writeValueAsString(map));
        out.flush();
        out.close();
    }
}

配置异常处理对象

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

 http.exceptionHandling()
            .authenticationEntryPoint(new DefaultAuthenticationEntryPoint())
            .accessDeniedHandler(new DefaultAccessDeniedExceptionHandler());

配置完成后,重启服务进行访问测试分析.

系统会话状态分析与实践

何为会话状态

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

会话状态如何存储

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

有状态的会话技术分析
Cookie 技术

Cookie是由服务端创建但在客户端存储会话状态的一个对象,此对象分为两种类型,一种为会话Cookie,一种为持久Cookie,浏览器在访问具体的某个域名时会携带这个域的有效Cookie到服务端.

会话Cookie: 浏览器关闭Cookie生命周期结束(一般默认都是会话Cookie)

持久Cookie: 持久Cookie是在Cookie对象创建时指定了生命周期,例如一周时间,即便浏览器关闭,持久Cookie依旧有效.

Session技术

Session技术由服务端创建,并在服务端存储会话状态的一个对象,当Session对象创建时,还会创建一个会话Cookie对象,并且通过这个会话Cookie将SessionId写到客户端,客户端下次访问服务端会携带这个会话Cookie,并且通过JsessionId找到Session对象,进而获取Session对象中存储的数据.Cookie默认的生命周期为30分钟.
在SpringSecurity中获取用户的认证信息,就可以通过如下方式进行实现:

Authentication authentication =
                SecurityContextHolder.getContext().getAuthentication();

无状态的会话技术分析

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值