zoukankan      html  css  js  c++  java
  • springSecurity

    Spring Security

    第一章 SpringSecurity-简介

    SpringSecurity融合Spring技术栈,提供JavaEE应 用的整体安全解决方案;

    SpringSecurity的安全内容主要包含两块内容授权(authorization)和认证(authentication)首先用户登录的时候传入登录信息,登录验证器完成登录认证并将登录认证好的信息存储到请求上下文,然后再进行其他操作,如在进行接口访问、方法调用时,权限认证器从上下文中获取登录认证信息,然后根据认证信息获取权限信息,通过权限信息和特定的授权策略决定是否授权。

    四种配置方式

    一种是全部利用配置文件,将用户、权限、资源(url)硬编码在xml文件中

    二种是用户和权限用数据库存储,而资源(url)和权限的对应采用硬编码配置

    三种是细分角色和权限,并将用户、角色、权限和资源均采用数据库存储,并且自定义过滤器,代替原有的FilterSecurityInterceptor过滤器, 并分别实现AccessDecisionManager、InvocationSecurityMetadataSourceService和UserDetailsService,并在配置文件中进行相应配置。

    四是修改springsecurity的源代码,主要是修改InvocationSecurityMetadataSourceService和UserDetailsService两个类。

    ​ InvocationSecurityMetadataSourceService

    ​ 将配置文件或数据库中存储的资源(url)提取出来加工成为url和权限列表的Map供Security使用

    ​ UserDetailsService

    ​ 提取用户名和权限组成一个完整的(UserDetails)User对象,该对象可以提供用户的详细信息供AuthentationManager进行认证与授权使用

    第二章 SpringSecurity-HelloWorld

    测试环境搭建

    创建普通maven-war工程:spring-security-helloworld

    pom文件中添加依赖

    		<dependency>
    			<groupId>org.springframework</groupId>
    			<artifactId>spring-webmvc</artifactId>
    			<version>4.3.20.RELEASE</version>
    		</dependency>
    		<dependency>
    			<groupId>javax.servlet.jsp</groupId>
    			<artifactId>jsp-api</artifactId>
    			<version>2.2</version>
    			<scope>provided</scope>
    		</dependency>
    		<dependency>
    			<groupId>javax.servlet</groupId>
    			<artifactId>servlet-api</artifactId>
    			<version>2.5</version>
    			<scope>provided</scope>
    		</dependency>
    		<dependency>
    			<groupId>javax.servlet</groupId>
    			<artifactId>jstl</artifactId>
    			<version>1.2</version>
    		</dependency>
    
    

    在web.xml中配置

    <servlet>
    <servlet-name>springDispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
    <servlet-name>springDispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
    </servlet-mapping>
    
    

    配置Spring:spring.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd
    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">
    <context:component-scan base-package="com.atguigu.security"></context:component-scan>
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/views/"></property>
    <property name="suffix" value=".jsp"></property>
    </bean>
    <mvc:annotation-driven />
    <mvc:default-servlet-handler />
    </beans>
    
    

    编写controller可以正常访问

    引入Spring Security 框架

    添加依赖
    <dependency>
    	<groupId>org.springframework.security</groupId>
    	<artifactId>spring-security-web</artifactId>
    	<version>4.2.10.RELEASE</version>
    </dependency>
    <dependency>
    	<groupId>org.springframework.security</groupId>
    	<artifactId>spring-security-config</artifactId>
    	<version>4.2.10.RELEASE</version>
    </dependency>
    <!-- 标签库 -->
    <dependency>
    	<groupId>org.springframework.security</groupId>
    	<artifactId>spring-security-taglibs</artifactId>
    	<version>4.2.10.RELEASE</version>
    </dependency>
    
    

    web.xml中添加SpringSecurity的Filter进行安全控制

    <filter>
    	<filter-name>springSecurityFilterChain</filter-name><!--名称固定,不能变-->
    	<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
    	<filter-name>springSecurityFilterChain</filter-name>
    	<url-pattern>/*</url-pattern>
    </filter-mapping>
    
    

    加入SpringSecurity配置类

    @Configuration、@Bean 注解作用

    @Configuration
    @EnableWebSecurity
    public class AppWebSecurityConfig extends WebSecurityConfigurerAdapter {
    }
    
    

    启动效果

    • 所有资源访问受限(包括静态资源)
    • 需要一个默认的登录页面(框架自带的)
    • 账号密码错误会有提示
    • 查看登录页面的源码,发现有个hidden-input;name="_csrf" 这是SpringSecurity帮我们防止“跨站请求伪造”攻击;还可以防止表单重复提交。

    image-20201125082703439

    image-20201125082720388

    第三章 SpringSecurity-实验

    Spring Security 的实验

    实验一:授权首页和静态资源

    • 配置类(AppWebSecurityConfig extends WebSecurityConfigurerAdapter

    • 重写configure(HttpSecurity http)****方法

    @Configuration
    @EnableWebSecurity
    public class AppWebSecurityConfig extends WebSecurityConfigurerAdapter {
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		// super.configure(http); //取消默认配置
    		http.authorizeRequests()
                .antMatchers("/layui/**", "/index.jsp")
                .permitAll() // 设置匹配的资源放行
                .anyRequest().authenticated(); // 剩余任何资源必须认证
    	}
    }
    
    
    
    测试结果
    • 静态资源和index.jsp都可以访问

    • 不存在的资源

    有权限无资源404

    image-20201125082928141

    无权限

    image-20201125082943062

    实验二:默认及自定义登录页

    开启formLogin()功能 默认主页时

    • 静态资源和index.jsp都可以访问

    不存在的资源

    暂时禁用csrf:http.csrf().disable();

    登录逻辑分析

    /**默认登录页面

    * /login GET - the login form

    * /login POST - process the credentials and if valid authenticate the user

    * /login?error GET - redirect here for failed authentication attempts

    * /login?logout GET - redirect here after successfully logging out

    * 定制登录页面:loginPage("/index.jsp"):规定登录页的地址在哪里

    * /index.jsp GET - the login form

    * /index.jsp POST - process the credentials and if valid authenticate the user

    * /index.jsp?error GET - redirect here for failed authentication attempts

    * /index.jsp?logout GET - redirect here after successfully logging out

    * ${SPRING_SECURITY_LAST_EXCEPTION.message}可以取出错误消息

    */

    测试结果

    image-20201125084023183

    CSRF跨站请求伪造

    SpringSecurity添加了csrf功能【DefaultCsrfToken】,所有的表单提交为了防止跨站请求伪造,我们需要加上_csrf项; 或者,暂时禁用http.csrf().disable();

    • ${_csrf} ===>>> org.springframework.security.web.csrf.DefaultCsrfToken@19116cfd

    如果不禁用csrf,默认是开启的状态;页面不设置csrf表单域,那么,提交登录请求会报错

    image-20201125084420116

    令牌值变化:

    • 如果登录成功(用户名,密码正确),令牌会被删除,

    • 重新回到登录页或后退网页,令牌会重新生成;

    • 如果登录失败(用户名,密码错误),令牌不变。

    • 刷新登录页,令牌值也不变

    作用:

    • 防止别的网站伪造数据,提交请求到我们的网站。

      image-20201125084644267

    扩展-了解XSS

    • XSS攻击全称跨站脚本攻击,是为不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为XSS,XSS是一种在web应用中的计算机安全漏洞,它允许恶意web用户将代码植入到提供给其它用户使用的页面中。

    CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与XSS非常不同,XSS利用站点内的信任用户,而CSRF则通过伪装成受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。

    实验五:用户注销完成

    添加注销功能(logout)http.logout()默认规则

    1. /logout:退出系统

    2. 如果csrf开启,必须post方式的/logout请求,表单中需要增加csrf token

    3. logoutUrl();退出系统需要发送的请求

    4. logoutSuccessUrl();退出系统成功以后要跳转的页面地址

    5. addLogoutHandler():自定义注销处理器

    6. deleteCookies():指定需要删除的cookie

    7. invalidateHttpSession():session失效(DEBUG)

    实验六:基于角色的访问控制

    设置资源可以访问的角色

    http.authorizeRequests().antMatchers("/layui/**","/index.jsp").permitAll() //允许所有人都访问静态资源,无论登录(认证)与否
    .antMatchers("/level1/**").hasRole("学徒")
    .antMatchers("/level2/**").hasRole("大师")
    .antMatchers("/level3/**").hasRole("宗师")
    .anyRequest().authenticated(); //放置最后,以上没有规定的都需要权限认证。
    
    

    注意:

    • 将.anyRequest().authenticated()错误的设置在前面,后面的设置就不起作用了。

    • 设置所有,"/**"都可以访问,其他再进行的设置就不会起作用了

    • 设置匿名访问/level3/** 可以不用登录,匿名访问:.anyRequest().anonymous();

    拥有该角色的资源可以访问,否则不可以访问

    auth.inMemoryAuthentication()
    
    .withUser("zhangsan").password("123456").roles("ADMIN","学徒","宗师")
    
    .and()
    
    .withUser("自定义访问拒绝处理页面,lisi").password("111111").authorities("USER","MANGER");
    

    实验七:自定义访问拒绝处理页面

    直接增加处理映射界面

    http.exceptionHandling().accessDeniedPage("/unauth.html");  
    

    在控制器类中增加映射处理

    @RequestMapping("/unauth.html")  
    public String unauth(){  
        return "unauth";  
    }  
    

    增加显示页面,将main.jsp复制,命名为unauth.jsp,增加一句提示信息

    <h1>你无权访问该页面...</h1>  
    

    测试显示效果

    自定义异常处理器

    image-20201125085525282

    实验八:记住我功能

    记住我功能-免登录原理

    http.rememberMe();

    默认规则

    • 页面checkbox提交remember-me参数
    • 默认记住2周登录状态:AbstractRememberMeServices
    • image-20201125085848816
    • 会在cookie中保存名为:remember-me的cookie
      记住了以前登录的状态,以后再访问就不用登录了

    登录后页面,关闭浏览器,直接访问:

    http://localhost/spring-security-helloworld/main.html 可以成功访问,不必登录。

    这种方式,token值是放置在内存中的,服务器端重启tomcat,token会失效。需要将token记录在数据库持久化才不会失效。

    记住我-数据版

    引入pom.xml 数据包

    <dependency>
    	<groupId>org.springframework</groupId>
    	<artifactId>spring-orm</artifactId>
    	<version>4.3.20.RELEASE</version>
    </dependency>
    <dependency>
    	<groupId>com.alibaba</groupId>
    	<artifactId>druid</artifactId>
    	<version>1.1.12</version>
    </dependency>
    <!-- mysql驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
    
    

    配置数据源

    <!-- 配置数据源 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="username" value="root"></property>
    <property name="password" value="root"></property>
    <property name="url" value="jdbc:mysql://localhost/security?useSSL=false"></property>
    <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
    </bean> 
    <!--  jdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"></property>
    </bean>
    
    

    创建表

    create table persistent_logins (username varchar(64) not null, series varchar(64) primary key,token varchar(64) not null, last_used timestamp not null)
    

    image-20201125092640834

    设置记住我

    @Autowired
    DataSource dataSource;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    //。。。
    //记住我
    JdbcTokenRepositoryImpl ptr = new JdbcTokenRepositoryImpl();
    ptr.setDataSource(dataSource);
    http.rememberMe().tokenRepository(ptr);
    }
    
    

    image-20201125092729581

    自定义认证用户信息

    auth.inMemoryAuthentication()
    .withUser("zhangsan").password("123456").roles("ADMIN")
    .and()
    .withUser("lisi").password("123123").authorities("USER","MANAGER");
    
    

    第四章 认证

    使用数据库保存/查询用户数据,完成认证功能

    方式一:重写jdbcAuthentication规则(不推荐)
    • 基于数据库的RBAC查询出我们需要的用户以及这些用户的权限(权限标识、角色)

    • 创建和SpringSecurity要求一模一样的表,然后用默认jdbcAuthentication

    • 更新jdbcAuthentication里面所有我们需要实际运行的sql

    • authoritiesByUsernameQuery:根据用户名查询他权限的sql

    • usersByUsernameQuery:根据用户名查询用户的sql

    • .......:更多的sql均可定义

      使用默认的查询用户语句

      auth.jdbcAuthentication().usersByUsernameQuery("zhangsan");

    image-20201125093130625

    使用默认的查询权限语句

    auth.jdbcAuthentication().authoritiesByUsernameQuery("zhangsan");

    image-20201125093227810

    方式二:自定义UserDetailsService检索用户
    实现UserDetailService接口loadUserByUsername(String username)方法

    实验步骤

    创建表结构

    security实验security.sql

    配置 configure(AuthenticationManagerBuilder auth)

    @Autowired
    UserDetailsService userDetailsService;//用户详情查询服务组件的接口
     
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    //根据用户名查询出用户的详细信息
    auth.userDetailsService(userDetailsService); 
    }
    
    

    编写UserDetailService实现:

      1. 接口及已有实现类
    • image-20201125102944046
    1. 实现UserDetailService接口,提供自定义实现类

    org.springframework.security.core.userdetails.UserDetailsService

    import java.util.Map;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.security.core.authority.*;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.stereotype.Service;
     
    //按照用户名查询用户详情的接口
    @Service
    public class AppUserDetailsServiceImpl implements UserDetailsService {
    	@Autowired
    	JdbcTemplate jdbcTemplate;
     
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    	String queryUser = "SELECT * FROM `t_admin` WHERE loginacct=?";
        
    	//1、查询指定用户的信息
    	Map<String, Object> map = jdbcTemplate.queryForMap(queryUser, username);
        
    	//2、将查询到的用户封装到框架使用的UserDetails里面
    	return new User(map.get("loginacct").toString(), map.get("userpswd").toString(), 
    	AuthorityUtils.createAuthorityList("ADMIN","USER"));//暂时写死,过后数据库中查
    }
    }
    
    
    debug测试登录-断点调试

    断点-方法栈

    image-20201125103523531

    自定义UserDetailService实现类

    image-20201125103537822

    Dao层认证提供者: DaoAuthenticationProvider

    Dao层认证提供者DaoAuthenticationProvider,用于调用自定义的UserDetailService实现类方法

    image-20201125103609936

    抽象层用户认证提供者: AbstractUserDetailsAuthenticationProvider

    抽象层用户认证提供者,获取dao层查找的认证用户信息,被封装成UserDetails对象,User类是UserDetails接口实现类

    image-20201125103634863

    1. org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.DefaultPreAuthenticationChecks认证用户账号是否被锁定,是否启用,是否过期;用户表中可以增加这些字段。

    image-20201125103656012

    1. public interface Authentication extends Principal 封装表单提交的认证信息

    image-20201125103725147

    • 认证用户名和密码;盐值为null

    image-20201125103742333

    • 采用org.springframework.security.authentication.encoding.BasePasswordEncoder默认加密器对表单提交明文加密(其实并没有进行任何加密,明文无变化)

    image-20201125103832791

    image-20201125103841152

    总结

    image-20201125103910202

    基于数据库(MD5密码)认证 (debug)

    使用数据库保存/查询用户数据,完成认证功能

    配置 configure(AuthenticationManagerBuilder auth)

    org.springframework.security.crypto.password.PasswordEncoder

    /测试:分析源码(验证密码不一致)
    auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    
    

    引入MD5加密工具类:MD5Util.java

    PasswordEncoder接口实现类:AppPasswordEncoder

    @Service
    public class AppPasswordEncoder implements PasswordEncoder {
     
    /**
       * 密码加密的算法
       */
    @Override
    public String encode(CharSequence rawPassword) {
    String digestPwd = MD5Util.digestPwd(rawPassword.toString());
    return digestPwd;
    }
     
    /**
       * 比较登录密码和数据库存储密码是否一致
       * rawPassword:页面的明文密码
       * encodedPassword:数据库的密文密码
       */
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
    //使用自己的工具类
    String digestPwd = MD5Util.digestPwd(rawPassword.toString());
    return digestPwd.equals(encodedPassword);
    } 
    }
    
    
    Debug测试,主要测试matches方法的调用过程

    表单提交密码:rawPassword

    image-20201125104200029

    数据库存储密码 :encodePassword

    image-20201125104221209

    调用自定义密码验证器

    image-20201125104238029

    密码不一致,抛异常:Bad credentials ;密码一致,通过认证

    image-20201125104305512

    创建UsernamePasswordAuthenticationToken 对象,封装认证信息

    image-20201125104437983

    源码参考
    protected Authentication createSuccessAuthentication(Object principal,Authentication authentication, UserDetails user) {
    // Ensure we return the original credentials the user supplied,
    // so subsequent attempts are successful even with encoded passwords.
    // Also ensure we return the original getDetails(), so that future
    // authentication events after cache expiry contain the details 
    UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
    principal, authentication.getCredentials(),
    authoritiesMapper.mapAuthorities(user.getAuthorities())  );//封装用户权限信息 
    result.setDetails(authentication.getDetails()); //封装用户信息
    return result;
    }
     
    

    principal 认证主体-数据库中查询User数据

    image-20201125104512705

    authentication.getCredentials() 认证密码(表单中密码)

    image-20201125104526231

    authoritiesMapper.mapAuthorities(user.getAuthorities()) 认证权限集合

    该用户拥有的权限,暂时写死在代码中的,后期要根据用户查询所拥有的权限

    image-20201125104537450

    认证细节:包括客户端ip和sessionid

    org.springframework.security.web.authentication.WebAuthenticationDetails

    image-20201125104552911

    result对象(UsernamePasswordAuthenticationToken)详细描述

    image-20201125104612154

    基于数据库(BCryptPasswordEncoder)密码加密认证

    PasswordEncoder接口

    image-20201125104641220

    使用BCryptPasswordEncoder进行密码加密

    //推荐密码加密器用这个BCryptPasswordEncoder; 将一个字符串加密成一个永不重复的密文

    //1、加盐+加随机数

    auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());

    本地测试:main方法
    public static void main(String[] args) {
    BCryptPasswordEncoder pe = new BCryptPasswordEncoder();
     
    //$2a$10$WzKk37ncOPynOSxyFGkxWu3ys7xaf7L/9uUhfVYVOCFTqeHkgJvOq
    //$2a$10$VmWwIx/uxNQabCYl3I5mZ.U9sQvpiM/xAhX69Skg0EWyDm3twQfcO
    //$2a$10$2Ig1mxqlb033XcU7aB0Ck.OZouRLsHUkJyIl9Mzi40FIY6grcEUr6
    //大致的规律:$2a$10$+"xxx"+"/"+"xxx"
    String encode = pe.encode("123456");
    System.out.println(encode);
    }
    
    
    服务器运行测试

    将main方法生成的密文存储到数据库中(注意:userpswd字段长度),重新启动服务器进行测试。

    第五章 细粒度权限控制

    前置细节【Role和Authority的区别】

    5.1.1 用户拥有的权限表示

    roles("ADMIN","学徒","宗师")

    authorities("USER","MANAGER");

    5.1.2 给资源授予权限(角色或权限)

    //.antMatchers("/level1/**").hasRole("学徒")
    //.antMatchers("/level1/**").hasAnyRole("学徒","ADMIN")//拥有任何一个角色都可以访问
    .antMatchers("/level1/**").hasAnyAuthority("学徒","ADMIN") //拥有任何一个权限都可以访问
    .antMatchers("/level2/**").hasRole("大师")
    .antMatchers("/level3/**").hasRole("宗师")
    
    

    5.1.3 权限:【roles和authorities区别】

    • roles("ADMIN","学徒","宗师")

    增加"ROLE_"前缀存放:【"ROLE_ADMIN","ROLE_学徒","ROLE_宗师"】

    表示拥有的权限。一个角色表示的是多个权限

    用户传入的角色不能以ROLE_开头,否则会报错。ROLE_是自动加上的

    如果我们保存的用户的角色:直接传入角色的名字,权限【new SimpleGrantedAuthority("ROLE_" + role)】保存即可

    • authorities("USER","MANAGER");

    原样存放:【"USER","MANAGER"】

    表示拥有的权限。

    如果我们保存的是真正的权限;直接传入权限名字,权限【new SimpleGrantedAuthority(role)】保存

    无论是Role还是Authority都保存在 List,每个用户都拥有自己的权限集合->List

    5.1.4 验证用户权限

    1. 通过角色(权限)验证:

    .antMatchers("/level1/**").hasRole("学徒")

    .antMatchers("/level1/**").hasAnyRole("学徒","ADMIN")

    拥有任何一个角色都可以访问

    验证时会自动增加"ROLE_"进行查找验证:【"ROLE_学徒","ROLE_ADMIN"】

    image-20201125105038178

    通过权限验证

    .antMatchers("/level1/**").hasAuthority("学徒")

    .antMatchers("/level1/**").hasAnyAuthority("学徒","ADMIN")

    拥有任何一个权限都可以访问

    验证时原样查找进行验证:【"学徒","ADMIN"】

    5.2 细粒度的资源控制

    • authenticated():通过认证的用户都可以访问

    • permitAll():允许所有人访问,即使未登录

    • authorizeRequests():更细粒度的控制

      • access(String): //SpEL:Spring表达式.access("hasRole('大师') AND hasAuthority('user:delete') OR hasIpAddress('192.168.50.15')")

    image-20201125105202378

    5.3 细粒度的资源控制相应注解

    使用注解与SpEl进行细粒度权限控制

    5.3.1 开启注解控制权限模式

    @EnableWebSecurity:开启 Spring Security 注解

    @EnableGlobalMethodSecurity(prePostEnabled=true):开启全局的细粒度方法级别权限控制功能

    5.3.2 几个权限检查注解

    1 @PreAuthorize:方法执行前检查

    @PreAuthorize("hasRole('ADMIN')")  
    public void addUser(User user){  
        //如果具有ROLE_ADMIN 权限 则访问该方法  
        ....  
    }
    
    

    2 @PostAuthorize:方法执行后检查,失败抛异常

    @PostAuthorize:允许方法调用,但是,如果表达式结果为false抛出异常  
    //returnObject可以获取返回对象user,判断user属性username是否和访问该方法的用户对象的用户名一样。不一样则抛出异常。  
    @PostAuthorize("returnObject.user.username==principal.username")  
    public User getUser(int userId){  
       //允许进入
    ...  
        return user;
    }
    
    

    3 @PostFilter:允许方法调用,但是按照表达式过滤方法结果

     //将结果过滤,即选出性别为男的用户  
    @PostFilter("returnObject.user.sex=='男' ")  
    public List<User> getUserList(){  
        //允许进入
        ...  
        return user; 
    }
    
    

    4 @PreFilter:允许方法调用,但必须在进入方法前过滤输入值

    5 @Secured:拥有指定角色才可以访问方法

    @Secured('ADMIN')   等价于    @PreAuthorize("hasRole('ADMIN')") 
    

    5.4 细粒度的资源控制注解中可写的表达式

    https://docs.spring.io/spring-security/site/docs/4.0.1.RELEASE/reference/htmlsingle/#el-common-built-in

    所有能使用的表达式见上面文档连接

    image-20201125105420459

    5.5 细粒度权限控制实现步骤 ★

    5.5.1 开启全局的细粒度方法级别权限控制功能

    @EnableGlobalMethodSecurity(prePostEnabled = true)
    @EnableWebSecurity
    @Configuration
    public class AppSecurityConfig extends WebSecurityConfigurerAdapter {
    }
    
    

    5.5.2 将手动授权的方式注释掉

    //.antMatchers("/level1/**").hasRole("学徒")
    //.antMatchers("/level1/**").hasAnyRole("学徒","ADMIN")
    //.antMatchers("/level1/**").hasAnyAuthority("学徒","ADMIN")
    //.antMatchers("/level1/**").hasAuthority("学徒")
    //.antMatchers("/level2/**").hasRole("大师")
    //.antMatchers("/level3/**").hasRole("宗师")        
    
    

    5.5.3 给访问资源的方法增加注解,进行访问授权

    @Controller
    public class GongfuController { 
    /**
       * 授权(权限检查)使用AOP;MethodSecurityInterceptor
       *                 方法执行之前AccessDecisionManager利用投票机制决定这个方法是否可运行
     * 
       */
    @PreAuthorize("hasRole('学徒') AND hasAuthority('luohan')")
    @GetMapping("/level1/1")
    public String leve1Page(){
    return "/level1/1";
    }
    @PreAuthorize("hasRole('学徒') AND hasAuthority('wudang')")
    @GetMapping("/level1/2")
    public String leve1Page2(){
    return "/level1/2";
    }
    @PreAuthorize("hasRole('学徒') AND hasAuthority('quanzhen')")
    @GetMapping("/level1/3")
    public String leve1Page3(){
    return "/level1/3";
    }
    }
    
    

    5.5.4 通过数据库加载用户权限

    @Service
    public class AppUserDetailsServiceImpl implements UserDetailsService {
    
    @Autowired
    JdbcTemplate jdbcTemplate ;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    String sql = "select  * from t_admin where loginacct=?";
    Map<String, Object> map = jdbcTemplate.queryForMap(sql, username);
    
    //查询用户拥有的角色集合
    String sql1="SELECT t_role.* FROM t_role LEFT JOIN t_admin_role ON t_admin_role.roleid=t_role.id WHERE t_admin_role.adminid=?";                
    List<Map<String, Object>> roleList = jdbcTemplate.query(sql1, new ColumnMapRowMapper(), map.get("id"));
    
    //查询用户拥有的权限集合
    String sql2 = "SELECT distinct t_permission.* FROM t_permission LEFT JOIN t_role_permission ON t_role_permission.permissionid = t_permission.id LEFT JOIN t_admin_role ON t_admin_role.roleid=t_role_permission.roleid WHERE t_admin_role.adminid=?";
    List<Map<String, Object>> permissionList = jdbcTemplate.query(sql2, new ColumnMapRowMapper(), map.get("id"));
    
    //用户权限=【角色+权限】
    Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
                   
    for (Map<String, Object> rolemap : roleList) {
    String rolename = rolemap.get("name").toString();
    authorities.add(new SimpleGrantedAuthority("ROLE_"+rolename));
    }                
    for (Map<String, Object> permissionmap : permissionList) {
    String permissionName = permissionmap.get("name").toString();
    if(!StringUtils.isEmpty(permissionName)) {
    authorities.add(new SimpleGrantedAuthority(permissionName));
    }                        
    }
    
    //return new User(map.get("loginacct").toString(),map.get("userpswd").toString(),
    //AuthorityUtils.createAuthorityList("ADMIN","USER"));
    return new User(map.get("loginacct").toString(),map.get("userpswd").toString(),authorities);
    }
    }
    
    

    .5.5 准备数据

    image-20201125105534370

    5.5.6 测试结果

    登录认证通过,可以登录到成功页面

    访问【学徒】角色下的资源:

    /level1/1 罗汉拳不可以访问

    /level1/2 武当长拳可以访问

    /level1/3 全真剑法不可以访问

    第六章 SpringSecurity-原理

    认证原理-UsernamePasswordAuthenticationFilter

    1、获取到页面的用户名和密码

    2、将username和password包装成UsernamePasswordAuthenticationToken

    3、获取系统的认证管理器(AuthenticationManager)来调用authenticate方法完成认证

    3.1)AuthenticationManager获取ProviderManager来调用ProviderManager.authenticate()

    3.2)ProviderManager获取到所有的AuthenticationProvider判断当前的提供者能否支持,如果支持provider.authenticate(authentication);

    现在我们DaoAuthenticationProvider( authentication :页面封装用户名和密码的对象

    3.2.1)retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);

    3.2.1.1)loadedUser = this.getUserDetailsService().loadUserByUsername(username);

    (调用我们自己的UserDetailsService****来去数据库查用户,按照用户名查出来的用户的详细信息)封装成UserDetails

    3.2.2)preAuthenticationChecks.check(user);(预检查,账号是否被锁定等…)

    3.2.3)additionalAuthenticationChecks(附加的认证检查)

    3.2.3.1)使用passwordEncoder. matches检查页面的密码和数据库的密码是否一致

    3.2.4)postAuthenticationChecks.check(user);(后置认证,检查密码是否过期)

    3.2.5)createSuccessAuthentication:将认证成功信息重新封装成UsernamePasswordAuthenticationToken

    3.3)3.2又返回了一个新的UsernamePasswordAuthenticationToken,然后擦掉密码

    4、 eventPublisher.publishAuthenticationSuccess(result);告诉所有监听器认证成功了

    UsernamePasswordAuthencationFilter源码流程-带图

    执行过滤器,获取到页面的用户名和密码

    public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter

    image-20201124200722072

    将username和password包装成UsernamePasswordAuthenticationToken

    image-20201124200922440

    获取系统的认证管理器(AuthenticationManager)来调用authenticate方法完成认证(this.getAuthenticationManager().authenticate(authRequest))

    AuthenticationManager获取ProviderManager来调用ProviderManager.authenticate()

    image-20201124201028772

    ProviderManager获取到所有的AuthenticationProvider判断当前的提供者能否支持,如果支持provider.authenticate(authentication);

    DaoAuthenticationProvider( authentication :页面封装用户名和密码的对象)

    image-20201124201203081

    3.2.1)、retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);

    3.2.1.1)、 loadedUser = this.getUserDetailsService().loadUserByUsername(username);

    (调用我们自己的UserDetailsService来去数据库查用户,按照用户名查出来的用户的详细信息)封装成UserDetail

    3.2.2)、 preAuthenticationChecks.check(user);(预检查,账号是否被锁定等…)

    3.2.3)、 additionalAuthenticationChecks(附加的认证检查)

    3.2.3.1)、使用passwordEncoder. matches检查页面的密码和数据库的密码是否一致

    3.2.4)、 postAuthenticationChecks.check(user);(后置认证,检查密码是否过期)

    3.2.5)、 createSuccessAuthentication:将认证成功的信息重新封装成UsernamePasswordAuthenticationToken

    3.3)、 3.2又返回了一个新的UsernamePasswordAuthenticationToken,然后擦掉密码

    4 eventPublisher.publishAuthenticationSuccess(result);认证成功

    image-20201124201303725

    5 successfulAuthentication(request, response, chain, authResult);

    通过调用 SecurityContextHolder.getContext().setAuthentication(...) 将 Authentication 对象赋给当前的 SecurityContext

    image-20201124201321561

    org.springframework.security.core.context.SecurityContextHolderStrategy

    image-20201124201337237

    ThreadLocal线程数据绑定

    image-20201124201349486

    SecurityContextHolder.getContext();就能获取到之前认证好的Authentication对象(UsernamePasswordAuthenticationToken)

    6断点参考

    image-20201124201409445

    认证原理-流程及相关类(API)

    认证&授权

    6.4.1 认证流程

    1. 用户使用用户名和密码登录
    2. 用户名密码被过滤器(默认为 UsernamePasswordAuthenticationFilter)获取到,封装成 Authentication(UsernamePasswordAuthenticationToken)
    3. token(Authentication实现类)传递给 AuthenticationManager 进行认证
    4. AuthenticationManager 认证成功后返回一个封装了用户权限信息的 Authentication 对象
    5. 通过调用 SecurityContextHolder.getContext().setAuthentication(...) 将 Authentication 对象赋给当前的 SecurityContext
    6. 将用户的信息保存到当前线程上,共享起来
    7. SecurityContextHolder.getContext();就能获取到之前认证好的Authentication对象(UsernamePasswordAuthenticationToken)

    6.4.2默认执行顺序

    UsernamePasswordAuthenticationFilter

    1. 用户通过url:/login 登录,该过滤器接收表单用户名密码

    2. 判断用户名密码是否为空

    3. 生成 UsernamePasswordAuthenticationToken

    4. 将 Authentiction 传给 AuthenticationManager接口的 authenticate 方法进行认证处理

    5. AuthenticationManager 默认是实现类为 ProviderManager ,ProviderManager 委托给 AuthenticationProvider 进行处理

    6. UsernamePasswordAuthenticationFilter 继承了 AbstractAuthenticationProcessingFilter 抽象类,AbstractAuthenticationProcessingFilter 在 successfulAuthentication 方法中对登录成功进行了处理,通过 SecurityContextHolder.getContext().setAuthentication() 方法将 Authentication 认证信息对象绑定到 SecurityContext

    7. 下次请求时,在过滤器链头的 SecurityContextPersistenceFilter 会从 Session 中取出用户信息并生成 Authentication(默认为 UsernamePasswordAuthenticationToken),并通过 SecurityContextHolder.getContext().setAuthentication() 方法将 Authentication 认证信息对象绑定到 SecurityContext

    需要权限才能访问的请求会从 SecurityContext 中获取用户的权限进行验证

    2 DaoAuthenticationProvider(实现了 AuthenticationProvider)

    通过 UserDetailsService 获取 UserDetails将 UserDetails 和 UsernamePasswordAuthenticationToken 进行认证匹配用户名密码是否正确若正确则通过 UserDetails 中用户的权限、用户名等信息生成新的 Authentication 认证对象并返回

    6.4.3 相关类

    1. WebSecurityConfigurerAdapter(配置使用)

    为创建 WebSecurityConfigurer 实例提供方便的基类,重写它的 configure 方法来设置安全细节 configure(HttpSecurity http):重写该方法,通过 http 对象的 authorizeRequests()方法定义URL访问权限,默认会为 formLogin() 提供一个简单的测试HTML页面 configure (AuthenticationManagerBuilder auth):通过 auth 对象的方法添加身份验证

    2. SecurityContextHolder

    SecurityContextHolder 用于存储安全上下文信息(如操作用户是谁、用户是否被认证、用户权限有哪些),它用 ThreadLocal 来保存 SecurityContext,者意味着 Spring Security 在用户登录时自动绑定到当前现场,用户退出时,自动清除当前线程认证信息,SecurityContext 中含有正在访问系统用户的详细信息

    3. AuthenticationManagerBuilder

    用于构建认证 AuthenticationManager 认证,允许快速构建内存认证、LDAP身份认证、JDBC身份验证,添加 userDetailsService(获取认证信息数据) 和 AuthenticationProvider's(定义认证方式)

    4. UserDetailsService

    该接口仅有一个方法 loadUserByUsername,Spring Security 通过该方法获取用户信息

    5. UserDetails

    代表了Spring Security的用户实体类,带有用户名、密码、权限特性等性质,可以自己实现该接口,供 Spring Security 安全认证使用,Spring Security 默认使用的是内置的 User 类 将从数据库获取的 User 对象传入实现该接口的类,并获取 User 对象的值来让类实现该接口的方法 通过 Authentication.getPrincipal() 的返回类型是 Object,但很多情况下其返回的其实是一个 UserDetails 的实例

    6. Authentication

    Authentication 是一个接口,用来表示用户认证信息,在用户登录认证之前相关信息(用户传过来的用户名密码)会封装为一个 Authentication 具体实现类对象,默认情况下为 UsernamePasswordAuthenticationToken,登录之后(通过AuthenticationManager认证)会生成一个更详细的、包含权限的对象,然后把它保存在权限线程本地的 SecurityContext 中,供后续权限鉴定用 Authentication.principal可以获取到已经认证的用户详细信息 UsernamePasswordAuthenticationToken (密码被擦除,authenticated=true)

    7. GrantedAuthority

    GrantedAuthority 是一个接口,它定义了一个 getAuthorities() 方法返回当前 Authentication 对象的拥有权限字符串,用户有权限是一个 GrantedAuthority 数组,每一个 GrantedAuthority 对象代表一种用户权限

    8. AuthenticationManager

    AuthenticationManager 是用来处理认证请求的接口,它只有一个方法 authenticate(),该方法接收一个 Authentication 作为对象,如果认证成功则返回一个封装了当前用户权限信息的 Authentication 对象进行返回 它默认的实现是 ProviderManager,但它不处理认证请求,而是将委托给 AuthenticationProvider 列表,然后依次使用 AuthenticationProvider 进行认证,如果有一个 AuthenticationProvider 认证的结果不为null,则表示成功(否则失败,抛出 ProviderNotFoundException),之后不在进行其它 AuthenticationProvider 认证,并作为结果保存在 ProviderManager 认证校验时最常用的方式就是通过用户名加载 UserDetails,然后比较 UserDetails 密码与请求认证是否一致,一致则通过,Security 内部的 DaoAuthenticationProvider 就是使用这种方式 认证成功后加载 UserDetails 来封装要返回的 Authentication 对象,加载的 UserDetails 对象是包含用户权限等信息的。认证成功返回的 Authentication 对象将会保存在当前的 SecurityContext

    9. AuthenticationProvider

    AuthenticationProvider 是一个身份认证接口,实现该接口来定制自己的认证方式,可通过 UserDetailsSevice 对获取数据库中的数据 该接口中有两个需要实现的方法: Authentication authenticate(Authentication authentication):认证处理,返回一个 Authentication 的实现类则代表成功,返回 null 则为认证失败 supports(Class<?> aClass):如果该 AuthenticationProvider 支持传入的 Authentication 认证对象,则返回 true ,如:return aClass.equals(UsernamePasswordAuthenticationToken.class);

    image-20201124202017830

    10. AuthorityUtils

    是一个工具包,用于操作 GrantedAuthority 集合的实用方法: commaSeparatedStringToAuthorityList(String authorityString):从逗号分隔符中创建 GrantedAuthority 对象数组,帮我们快速创建出权限的集合

    11.AbstractAuthenticationProcessingFilter

    该抽象类继承了 GenericFilterBean,是处理 form 登录的过滤器,与 form 登录相关的所有操作都在该抽象类的子类中进行(UsernamePasswordAuthenticationFilter 为其子类),比如获取表单中的用户名、密码,然后进行认证等操作 该类在 doFilter 方法中通过 attemptAuthentication() 方法进行用户信息逻辑认证,认证成功会将用户信息设置到 Session 中

    12. HttpSecurity

    用于配置全局 Http 请求的权限控制规则,对哪些请求进行验证、不验证等 常用方法:

    authorizeRequests():返回一个配置对象用于配置请求的访问限制

    formLogin():返回表单配置对象,当什么都不指定时会提供一个默认的,如配置登录请求,还有登录成功页面

    logout():返回登出配置对象,可通过logoutUrl设置退出url

    antMatchers:匹配请求路径或请求动作类型,如:.antMatchers("/admin/**")

    addFilterBefore: 在某过滤器之前添加 filter

    addFilterAfter:在某过滤器之后添加 filter

    addFilterAt:在某过滤器相同位置添加 filter,不会覆盖相同位置的 filter

    hasRole:结合 antMatchers 一起使用,设置请求允许访问的角色权限或IP,如:

    .antMatchers("/admin/**").hasAnyRole("ROLE_ADMIN","ROLE_USER")

    方法名 用途
    access(String) SpringEL表达式结果为true时可访问
    anonymous() 匿名可访问
    denyAll() 用户不可以访问
    fullyAuthenticated() 用户完全认证访问(非remember me下自动登录)
    hasAnyAuthority(String…) 参数中任意权限可访问
    hasAnyRole(String…) 参数中任意角色可访问
    hasAuthority(String) 某一权限的用户可访问
    hasRole(String) 某一角色的用户可访问
    permitAll() 所有用户可访问
    rememberMe() 允许通过remember me登录的用户访问
    authenticated() 用户登录后可访问
    hasIpAddress(String) 用户来自参数中的IP可访问

    1. PasswordEncoder

    Spring 提供的一个用于对密码加密的接口,首选实现类为 BCryptPasswordEncoder

    2. BCryptPasswordEncoder

    spring security中的BCryptPasswordEncoder方法采用SHA-256 +随机盐+密钥 对密码进行加密。SHA系列是Hash算法,不是加密算法,使用加密算法意味着可以解密(这个与编码/解码一样),但是采用Hash处理,其过程是不可逆的。 (1)加密****(encode):注册用户时,使用SHA-256+随机盐+密钥把用户输入的密码进行hash处理,得到密码的hash值,然后将其存入数据库中。 (2)密码匹配****(matches):用户登录时,密码匹配阶段并没有进行密码解密(因为密码经过Hash处理,是不可逆的),而是使用相同的算法把用户输入的密码进行hash处理,得到密码的hash值,然后将其与从数据库中查询到的密码hash值进行比较。如果两者相同,说明用户输入的密码正确。 这正是为什么处理密码时要用hash算法,而不用加密算法。因为这样处理即使数据库泄漏,黑客也很难破解密码

    6.5 授权原理-AOP-MethodSecurityInterceptor

    MethodSecurityInterceptor(基于AOP模式进行权限检查)

    ​ 授权(权限检查机制)采用AOP机制:

    org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor

    ​ 方法执行前org.springframework.security.access.AccessDecisionManager通过投票机制决定这个方法是否可以被执行

    例如

    @PreAuthorize(value="hasRole('学徒') AND hasAuthority('luohan')")

    @GetMapping("/level1/1")

    public String leve1Page1(){

    return "/level1/1";

    }

    拦截器invoke方法

    image-20201124202558810

    支持各种功能的投票器

    image-20201124202628097

    投票器标识

    image-20201124202643887

    AccessDecisionManager利用系统中AccessDecisionVoter(投票器)进行授权操作:

    image-20201124202739055

    1)AffirmativeBased:有一个拒绝都不行

    2)ConsensusBased:赞成票数大于拒绝即可

    3)UnanimousBased:至少有一个赞成,不能全弃权和任何一个拒绝

  • 相关阅读:
    LINUX下文件编译
    Circle HDU
    Tree HDU
    Can you answer these queries? HDU
    Function HDU
    牛客练习赛46 A 华华教奕奕写几何 (简单数学)
    牛客练习赛26 E-树上路径 (树链剖分+线段树)
    Flight HDU
    牛客练习赛47 D DongDong坐飞机 (分层最短路)
    洛谷 P3384 【模板】树链剖分
  • 原文地址:https://www.cnblogs.com/mankaixin/p/14035258.html
Copyright © 2011-2022 走看看