Spring Security基础理论学习

前言

想着既然项目中用到Spring Security框架,那为了以后面试说到这个项目的时候不害怕被问框架问题,干脆就好好学一下,基础、实战、源码分析一个一个来。

参考资料

Spring Security参考手册:

https://www.springcloud.cc/spring-security-zhcn.html#true-java

https://docs.spring.io/spring-security/site/docs/5.1.12.RELEASE/reference/htmlsingle/#jc-authentication-userdetailsservice

Spring Security社区

http://www.spring4all.com/article/428

《Spring Security实战》

 

简介

Spring Security前身是Acegi Security,在被收纳为Spring子项目后正式更名为Spring Security

Spring Security,一种基于Spring AOP和Servlet过滤器的安全框架,可提供全面的安全性解决方案,在Web请求级和方法调用级处理身份认证和授权

Spring Security是一个框架,致力于为Java应用程序提供认证和授权,与所有Spring项目一样,SS真正强大之处在于可轻松扩展以满足自定义需求

其安全性体现在两方面:认证和授权,这两个概念并非Spring Security(后续简称SS)独有,只是在SS中可更便捷的完成认证和授权

  1. 认证:确认某主体在某系统中是否合法、可用,此处主体可为登录系统的用户,也可为接入的设备或其他系统 即验证登录 SS集成支持多种认证方式

    1. HTTP BASIC authentication headers:基于IETF RFC的标准

      1. IETF:Internet Engineering Task Force,Internet工程任务组,推动Internet标准规范制定的主要组织

      2. RFC:Request For Comments,请求注解,包含大多数关于Internet的重要文字资料,“网络知识圣经”

    2. LDAP:跨平台身份验证方式

    3. Form-based authentication:简单的用户界面需求

    4. OpenID authentication:去中心化的身份认证方式

    5. Jasig Central Authentication Service:单点登录方案

    6. Kerberos:使用对称密钥机制,允许客户端与服务器相互确认身份的认证协议

  2. 授权:主体通过认证后,是否允许其执行某项操作的过程 即赋予权限,SS支持基于URL对Web的请求授权,方法访问授权,对象访问授权等

SS的特征:

  1. 全面和可扩展的身份认证和授权支持

  2. 防止攻击,例如会话固定、CSRF、点击劫持等

    1. 会话固定:session fixation attack  利用应用系统在服务器的会话ID固定不变机制,借助他人用相同的会话ID获取认证和授权,然后利用该会话ID劫持他人的会话以成功冒充他人,造成会话固定攻击Spring Security基础理论学习_第1张图片

    2. CSRF:Cross-Site RequestrianForgery,跨站请求伪造,盗用用户的身份对服务器发送恶意请求,预防措施对Http request进行验证

    3. 点击劫持:视觉上的欺骗手段,攻击者使用一个透明的iframe或者用一张图片覆盖网页原有位置的含义,利用用户的手误注入操作,解决方法是HTTP头中禁止X-Frame-Option,检查img中的style

  3. 集成Servlet API

  4. 可选与Spring Web MVC集成

 

入门小程序

1. 使用SpringBoot 的Initializr搭建项目 https://start.spring.io/

1.1 Security作为构建项目的最小依赖

1.2 Web作为构建项目的核心

pom.xml依赖如下:


   org.springframework.boot
   spring-boot-starter-security


   org.springframework.boot
   spring-boot-starter-web

项目的结构图如下:

Spring Security基础理论学习_第2张图片

项目入口如下:

@RestController
@SpringBootApplication
public class SpringsecurityApplication {

   @GetMapping("/")
   public String testHello(){
      return "Hello, spring security";
   }

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

}

2. 如果不配置用户名和密码,系统默认登录用户名为user,密码为动态生成并打印到控制台的一串随机码

3. 如果配置用户名和密码,使用配置的用户名和密码进行认证

// application.properties
spring.security.user.name=wx
spring.security.user.password=ciery123

4. 实际中绝大多数Web应用不会使用HTTP基本认证这种方式,灵活性不足、安全性差、无法携带cookie。基本更倾向于选择表单认证,自己实现表单的登录页和验证逻辑从而提高安全性

 

基于表单的用户认证

1. 使用默认表单认证

1. SS支持HTTP基本认证和Form表单认证,如果不进行自定义配置只是单纯继承默认配置的话,SS默认使用Form表单认证

@EnableWebSecurity // 自带@Configuration,此处无需再加@Configutaion注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }
}

2. 重新运行程序,输入hello请求,自动跳转SS默认的login界面,输入用户名和密码,系统进行验证

  1. 如果认证成功则后端直接调回hello请求页面Spring Security基础理论学习_第3张图片
  2. 如果认证失败,前端提示用户名或密码错误Spring Security基础理论学习_第4张图片

2. 自定义表单登录页

1. 自定义http配置,配置loginPage,手动编写登录页面并放行登录页面,除开登录页面都需要进行认证(登录页面本身就是进行认证工作,如果还不给放行就离谱了)

@EnableWebSecurity // 自带@Configuration,此处无需再加@Configutaion注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
       // 自定义登录配置
        // authorizeRequests()方法返回一个URL拦截注册器,可调用其提供的anyRequest(),antMatchers()等方法匹配系统给的URL并为其制定安全策略
        http.authorizeRequests()
                // 任意一个http请求都会进行认证
                .anyRequest().authenticated()
                .and()
                // 设置表单验证登录
                .formLogin()
                // 自定义表单登录页 Security会用这个html注册一个POST路由,用于接收登录请求
                .loginPage("/login.html")
                // 设置登录页不设置访问限制,即登录页不用进行拦截
                .permitAll()
                .and()
                // 禁止csrf攻击
                .csrf().disable();
    }
}

2. 编写login.html静态页面,放在resources/static目录下




    
    login


    

3. 启动项目后,访问/hello资源跳转编写的login.html静态页面,提交用户名和密码,框架进行身份认证。如果认证成功则跳回/hello请求页面继续完成HTTP请求

Spring Security基础理论学习_第5张图片

4. 可指定处理登录请求的路径

4.1 如果需要自定义login的url,可以使用loginProcessingUrl进行配置

@EnableWebSecurity // 自带@Configuration,此处无需再加@Configutaion注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
       // 自定义登录配置
        // authorizeRequests()方法返回一个URL拦截注册器,可调用其提供的anyRequest(),antMatchers()等方法匹配系统给的URL并为其制定安全策略
        http.authorizeRequests()
                // 任意一个http请求都会进行认证
                .anyRequest().authenticated()
                .and()
                // 设置表单验证登录
                .formLogin()
                // 自定义表单登录页 Security会用这个html注册一个POST路由,用于接收登录请求
                .loginPage("/login.html")
                // 指定处理登录请求的路径
                .loginProcessingUrl("/login")
                // 设置登录页不设置访问限制,即登录页不用进行拦截
                .permitAll()
                .and()
                // 禁止csrf攻击
                .csrf().disable();
    }
}

4.2 login.html中需要修改action,url由/login.html改为自定义的"login"或"/login"

4.3 用户输入用户名和密码后,跳转自定义的认证路径完成认证 可以看到第3点中是login.html,此处已经被定义为login

Spring Security基础理论学习_第6张图片

5. 可自定义处理认证成功/失败

目前认证成功后,是由后端直接跳转返回之前访问的请求页面,但如果前后端完全分离的话仅靠JSON交互的系统中,这个是行不通的。这就需要后端在认证成功/失败后传给前端一个JSON,告诉前端目前登录认证的返回结果,前端根据这个结果进行相应的处理

可自定义实现SS自带的AuthenticationSuccessHandler和AuthenticationFailureHandler,利用HttpServletResponse对象将结果json反馈到页面上(后续前端是可以直接捕获这个json么?还是说需要后端调整将json传给前端?

@EnableWebSecurity // 自带@Configuration,此处无需再加@Configutaion注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
       // 自定义登录配置
        // authorizeRequests()方法返回一个URL拦截注册器,可调用其提供的anyRequest(),antMatchers()等方法匹配系统给的URL并为其制定安全策略
        http.authorizeRequests()
                // 任意一个http请求都会进行认证
                .anyRequest().authenticated()
                .and()
                // 设置表单验证登录
                .formLogin()
                // 自定义表单登录页 Security会用这个html注册一个POST路由,用于接收登录请求
                .loginPage("/login.html")
                // 指定处理登录请求的路径url
                .loginProcessingUrl("/login")
                // 指定登录成功时的处理逻辑 登录成功后后端传递json给前端,然后前端再处理,而非默认后端让其直接调回原页面
                .successHandler(new getAuthenticationSuccessHandler())
                .failureHandler(new getAuthenticationFailureHandler())
                // 设置登录页不设置访问限制,即登录页不用进行拦截
                .permitAll()
                .and()
                // 禁止csrf攻击
                .csrf().disable();
    }


    /**
     * 创建实现AuthenticationSuccessHandler认证成功处理器类
     */
    private class getAuthenticationSuccessHandler implements AuthenticationSuccessHandler{
            @Override
            public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                httpServletResponse.setContentType("application/json;charset=utf-8");
                // 构造json "key" : "value"
                httpServletResponse.getWriter().write("{\"error_code\":\"0\",\"message\":\"welcome\"}");
                httpServletResponse.sendRedirect("/hello");
            }
        }


    /**
     * 创建实现AuthenticationFailureHandler认证失败处理器类 由于两个处理器类只有一个方法,可以使用匿名内部类直接在configure方法中重写成功/失败的处理
     */
    private class getAuthenticationFailureHandler implements AuthenticationFailureHandler{
            @Override
            public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
            httpServletResponse.setContentType("application/json;charset=utf-8");
            // 401表示未登录
            httpServletResponse.setStatus(401);
            httpServletResponse.getWriter().write("{\"error_code\":\"401\",\"name\": \"" + e.getClass() + "\",\"message\":\"" + e.getMessage() + "\"}");
        }
    }
}

当然由于两个Handler只有一个方法,可以使用匿名内部类来实现对接口的重写

@EnableWebSecurity // 自带@Configuration,此处无需再加@Configutaion注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
       // 自定义登录配置
        // authorizeRequests()方法返回一个URL拦截注册器,可调用其提供的anyRequest(),antMatchers()等方法匹配系统给的URL并为其制定安全策略
        http.authorizeRequests()
                // 任意一个http请求都会进行认证
                .anyRequest().authenticated()
                .and()
                // 设置表单验证登录
                .formLogin()
                // 自定义表单登录页 Security会用这个html注册一个POST路由,用于接收登录请求
                .loginPage("/login.html")
                // 指定处理登录请求的路径
                .loginProcessingUrl("/login")
                // 指定登录成功时的处理逻辑 登录成功后后端传递json给前端,然后前端再处理,而非默认后端让其直接调回原页面
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                        httpServletResponse.setContentType("application/json;charset=utf-8");
                        // 构造json "key" : "value"
                        httpServletResponse.getWriter().write("{\"error_code\":\"0\",\"message\":\"welcome\"}");
                        httpServletResponse.sendRedirect("/hello");
                    }
                })
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                        httpServletResponse.setContentType("application/json;charset=utf-8");
                        // 401表示未登录
                        httpServletResponse.setStatus(401);
                        httpServletResponse.getWriter().write("{\"error_code\":\"401\",\"name\": \"" + e.getClass() + "\",\"message\":\"" + e.getMessage() + "\"}");
                    }
                })
                // 设置登录页不设置访问限制,即登录页不用进行拦截
                .permitAll()
                .and()
                // 禁止csrf攻击
                .csrf().disable();
    }

5.1 如果认证成功,返回信息到前端并跳转hello页面

Spring Security基础理论学习_第7张图片

5.2 如果认证失败,返回错误提示信息到前端

Spring Security基础理论学习_第8张图片

5.3 在successHandler中参数包含Authentication参数,携带当前登录的用户名和角色信息,可以根据需求进行处理,例如此处如果登录成功打印用户名

    /**
     * 创建实现AuthenticationSuccessHandler认证成功处理器类
     */
    private class getAuthenticationSuccessHandler implements AuthenticationSuccessHandler{
            @Override
            public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                httpServletResponse.setContentType("application/json;charset=utf-8");
                // 构造json "key" : "value" 注意 + 拼接的时候左右要加 \"
                httpServletResponse.getWriter().write("{\"error_code\":\"0\",\"message\":\"welcome, \"" + ((UserDetails)authentication.getPrincipal()).getUsername() + "\"}");
                // 最好后端不进行页面跳转,否则会覆盖json,直接返回json给前端,前端根据json信息进行处理即可

//                httpServletResponse.sendRedirect("/hello");
            }
        }

Spring Security基础理论学习_第9张图片

基于数据库的用户认证和授权

前提

1. 配置三个访问controller接口,其中user接口只可具有user权限的用户访问,admin接口只可具有admin权限的用户访问

@RestController
@RequestMapping("/app")
public class AppController {


    @GetMapping("/testApp")
    public String testApp(){
        return "hello,app";
    }
}

@RestController
@RequestMapping("/user")
public class UserController {


    @GetMapping("/testUser")
    public String testUser(){
        return "hello,user";
    }
}

@RestController
@RequestMapping("/admin")
public class AdminController {


    @GetMapping("/testAdmin")
    public String testAdmin(){
        return "hello,admin";
    }
}

2. 根据访问权限限制配置configure,使用antMatchers,填写需要访问的controller的url,添加访问角色验证

protected void configure(HttpSecurity http) throws Exception {
       // 自定义登录配置
        // authorizeRequests()方法返回一个URL拦截注册器,可调用其提供的anyRequest(),antMatchers()等方法匹配系统给的URL并为其制定安全策略
        http.authorizeRequests()
                /* antMatchers是一个采用ANT模式的URL匹配器
                    ?匹配任意单个字符
                    *匹配0或任意数量的字符
                    **匹配0或更多的目录
                    .antMatchers("/app/**") 相当于匹配/app/下所有的API
                    permitAll()表示公开权限
                    hasRole("role_name")限定只有方法参数role_name角色的用户才可访问
                */
                .antMatchers("/app/**").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasRole("USER")
                // 任意一个http请求都会进行认证
                .anyRequest().authenticated()
                .and()
                // 设置表单验证登录
                .formLogin()
                // 自定义表单登录页 Security会用这个html注册一个POST路由,用于接收登录请求
                .loginPage("/login.html")
                // 指定处理登录请求的路径
                .loginProcessingUrl("/login")
                // 设置登录页不设置访问限制,即登录页不用进行拦截
                .permitAll()
                .and()
                // 禁止csrf攻击
                .csrf().disable();
    }

3. 注意注释掉前面在application.properties中添加的默认username和password 开始创建多用户支持,分为基于内存、基于SS自带的JDBC数据库模型、基于自定义的数据库模型

1. 基于内存的多用户支持

由于基于内存所以每次重启项目内存被清空,用户都会重新创建。重启项目,user用户可以进入user/testUser,访问admin/testAdmin显示403错误

  1. 2xx:访问成功

  2. 4xx:浏览器错误 401未登录 403无权限 404无页面

  3. 5xx:服务器错误

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    // 使用明文,不对密码进行加密处理
    auth.inMemoryAuthentication().passwordEncoder(NoOpPasswordEncoder.getInstance())
    .withUser("user").password("123456").roles("USER")
    .and()
    .withUser("admin").password("123456").roles("ADMIN");
}

2. 基于数据库的多用户支持

由于数据库的主键限制,创建过的用户不可再通过configure自动创建(主键冲突)即重启项目就会报错。所以需要进行用户是否存在验证,如果存在则不作任何处理;不存在则创建

2.1 添加数据库依赖

//pom.xml


   org.springframework.boot
   spring-boot-starter-jdbc


   mysql
   mysql-connector-java

2.2 根据框架定义的数据表要求创建users和authorities表,唯一索引的作用是加快查询

Spring Security基础理论学习_第10张图片Spring Security基础理论学习_第11张图片Spring Security基础理论学习_第12张图片Spring Security基础理论学习_第13张图片

2.3 添加数据库连接信息

// application.properties 配置数据库连接信息
spring.datasource.url=jdbc:mysql://localhost:3306/springsecuritydemo?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT
spring.datasource.username=root
spring.datasource.password=root

2.4 创建用户信息

// WebSecurityConfig
@EnableWebSecurity // 自带@Configuration,此处无需再加@Configutaion注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    DataSource dataSource;


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 使用明文,不对密码进行加密处理
        // getUserDetailsService()方法从spring容器中获取jdbcManager
        auth.jdbcAuthentication().dataSource(dataSource).passwordEncoder(NoOpPasswordEncoder.getInstance()).getUserDetailsService();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
       // 自定义登录配置
        // authorizeRequests()方法返回一个URL拦截注册器,可调用其提供的anyRequest(),antMatchers()等方法匹配系统给的URL并为其制定安全策略
        http.authorizeRequests()
                .antMatchers("/app/**").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasRole("USER")
                // 任意一个http请求都会进行认证
                .anyRequest().authenticated()
                .and()
                // 设置表单验证登录
                .formLogin()
                // 自定义表单登录页 Security会用这个html注册一个POST路由,用于接收登录请求
                .loginPage("/login.html")
                // 指定处理登录请求的路径
                .loginProcessingUrl("/login")
                // 设置登录页不设置访问限制,即登录页不用进行拦截
                .permitAll()
                .and()
                // 禁止csrf攻击
                .csrf().disable();
    }

    @Bean
    public UserDetailsService userDetailsService(){
        JdbcUserDetailsManager manager = new JdbcUserDetailsManager();
        manager.setDataSource(dataSource);
        // 创建用户
        // 如果user存在则不作任何操作,如果不存在则创建用户,底层调用insert into users(usernmae,password,enable) values(?,?,?); 如果有role则调用insert into authorities (username, authority) values (?,?)
        if(!manager.userExists("user")){
            // User对象通过withUsername,password等方法给其赋值
            manager.createUser(User.withUsername("user").password("123456").roles("USER").build());
        }
        if(!manager.userExists("admin")){
            manager.createUser(User.withUsername("admin").password("123456").roles("ADMIN").build());
        }
        return manager;
    }
}

2.5 自动插入到users和authorites表中的数据

Spring Security基础理论学习_第14张图片Spring Security基础理论学习_第15张图片

拓展:数据能插入到数据库的原因:底层调用了JdbcTemplate类的相关insert/update方法将配置中的数据插入到数据表中

// JdbcUserDetailsManager

    private String createUserSql = "insert into users (username, password, enabled) values (?,?,?)";
    private String createAuthoritySql = "insert into authorities (username, authority) values (?,?)";


    public void createUser(UserDetails user) {
        this.validateUserDetails(user);
        this.getJdbcTemplate().update(this.createUserSql, (ps) -> {
            ps.setString(1, user.getUsername());
            ps.setString(2, user.getPassword());
            ps.setBoolean(3, user.isEnabled());
            int paramCount = ps.getParameterMetaData().getParameterCount();
            if (paramCount > 3) {
                ps.setBoolean(4, !user.isAccountNonLocked());
                ps.setBoolean(5, !user.isAccountNonExpired());
                ps.setBoolean(6, !user.isCredentialsNonExpired());
            }

        });
        if (this.getEnableAuthorities()) {
            this.insertUserAuthorities(user);
        }

    }

    private void insertUserAuthorities(UserDetails user) {
        Iterator var2 = user.getAuthorities().iterator();

        while(var2.hasNext()) {
            GrantedAuthority auth = (GrantedAuthority)var2.next();
            this.getJdbcTemplate().update(this.createAuthoritySql, new Object[]{user.getUsername(), auth.getAuthority()});
        }

    }

3. 自定义数据库模型

自定义UserDetails实现类User,然后自行通过UserServiceImpl类的loadByUsername方法进行登录认证

3.1 设计数据库user

Spring Security基础理论学习_第16张图片
3.2 添加mybatis依赖和自动映射mybatis-generator插件

// pom.xml



   org.mybatis.spring.boot
   mybatis-spring-boot-starter
   2.1.3



    
      org.mybatis.generator
      mybatis-generator-maven-plugin
      1.3.2
      
         
         true
         
         true
         
         src/main/resources/mybatis-generator.xml
      
      
         
            mysql
            mysql-connector-java
            8.0.17
         
      
   

3.3 创建映射配置文件并生成实体类User、Dao层UserMapper类、数据库映射文件UserMapper.xml

3.3.1 创建jdbc.properties添加数据库连接信息,之前的application.properties中的可以删去

// jdbc.properties

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/springsecuritydemo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT
jdbc.username=root
jdbc.password=root

3.3.2 mybatis-generator.xml

// mybatis-generator.xml








    
    


    
        
        
            
            
            
            
        
        
        
            
        
        
        
            
        
        
        
            
            
            
            
        
        
        
            
            
        
        
        
            
            
        
        
        

3.3.3 application.properties中添加mapper类的映射对应位置

// application.properties

#配置mybatis(相当于普通MyBatis中的SqlMapConfig.xml主配置文件中指定映射配置文件的地址和别称) 不配置会绑定数据失败,运行时无法找到mapper类下的方法(编译不报错,将实体bean加入spring容器后 解耦的效果)
mybatis.mapper-locations=classpath:/mapper/*.xml
mybatis.type-aliases-package= com.practice.mall.domain
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.use-generated-keys=true
mybatis.configuration.cache-enabled=false

3.3.4 开启mapper类的扫描

@MapperScan("com.practice.springsecurity.mapper")
@SpringBootApplication
public class SpringsecurityApplication {
	public static void main(String[] args) {
		SpringApplication.run(SpringsecurityApplication.class, args);
	}

}

3.4 实体类User实现UserDetails并添加List类型的GrantAuthority权限集属性

public class User implements UserDetails {
    private Long id;


    private String username;


    private String password;


    private Boolean enabled;


    private String roles;


    //权限
    private List authorities;


    public void setAuthorities(List authorities) {
        this.authorities = authorities;
    }


    public Long getId() {
        return id;
    }


    public void setId(Long id) {
        this.id = id;
    }


    public String getUsername() {
        return username;
    }


    @Override
    public boolean isAccountNonExpired() {
        return true;
    }


    @Override
    public boolean isAccountNonLocked() {
        return true;
    }


    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }


    @Override
    public boolean isEnabled() {
        return this.enabled;
    }


    public void setUsername(String username) {
        this.username = username == null ? null : username.trim();
    }


    @Override
    public Collection getAuthorities() {
        return this.authorities;
    }


    public String getPassword() {
        return password;
    }


    public void setPassword(String password) {
        this.password = password == null ? null : password.trim();
    }


    public Boolean getEnabled() {
        return enabled;
    }


    public void setEnabled(Boolean enabled) {
        this.enabled = enabled;
    }


    public String getRoles() {
        return roles;
    }


    public void setRoles(String roles) {
        this.roles = roles == null ? null : roles.trim();
    }
}

3.5 创建UserDetailsServiceImpl实现框架自带的UserDetailsService接口,重写loadByUsername方法

@Service //加入spring容器
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    UserMapper userMapper;


    /**
     * 根据username进行用户认证
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库中根据username查找用户
        User user = userMapper.selectByUsername(username);
        // 验证user是否存在
        if(user != null){
            // 将数据库中的role解析为权限集
            // AuthorityUtils.commaSeparatedStringToAuthorityList可将用逗号隔开的权限集字符串切割成可用的权限对象列表
            // 也可自定义实现切分方式,同步user表中的roles字段
            user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
            return user;
        }
        // 用户不存在
        throw new UsernameNotFoundException("用户不存在");
    }
}

3.6 在WebSecurityConfig配置类中将自定义的认证方式注入框架的authbuilder对象

@EnableWebSecurity // 自带@Configuration,此处无需再加@Configutaion注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    DataSource dataSource;
    @Autowired
    UserDetailsServiceImpl userDetailsServiceImpl;


    /**
     * 建立身份认证方式 AuthenticationManagerBuilder创建一个AuthenticationManager用于进行身份认证
     * 包括:内存验证、LDAP验证、基于JDBC的验证、添加UserDetailsService、添加AuthenticationProvider
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 使用明文,不对密码进行加密处理
        auth.userDetailsService(userDetailsServiceImpl).passwordEncoder(NoOpPasswordEncoder.getInstance());
    }

    /**
     * 开启认证 配置需要认证的url
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
       // 自定义登录配置
        // authorizeRequests()方法返回一个URL拦截注册器,可调用其提供的anyRequest(),antMatchers()等方法匹配系统给的URL并为其制定安全策略
        http.authorizeRequests()
                .antMatchers("/app/**").permitAll()
                // 限制既有ADMIN又有USER的用户访问
                .antMatchers("/admin/**").access("hasRole('ADMIN') and hasRole('USER')")
                .antMatchers("/user/**").hasRole("USER")
                // 任意一个http请求都会进行认证
                .anyRequest().authenticated()
                .and()
                // 设置表单验证登录
                .formLogin()
                // 自定义表单登录页 Security会用这个html注册一个POST路由,用于接收登录请求
                .loginPage("/login.html")
                // 指定处理登录请求的路径
                .loginProcessingUrl("/login")
                // 设置登录页不设置访问限制,即登录页不用进行拦截
                .permitAll()
                .and()
                // 禁止csrf攻击
                .csrf().disable();
    }

3.7 数据库user表中添加测试数据

Spring Security基础理论学习_第17张图片

3.8 测试

3.8.1 user登录admin/testAdmin,根据username从数据库获取user对象,并获取其roles匹配这个url需要的角色匹配不成功,则用户user登录成功但是无权限显示403

Spring Security基础理论学习_第18张图片

3.8.2 user登录user/testUesr,根据username从数据库获取user对象,并获取其roles匹配这个url需要的角色信息匹配成功,则用户user登录成功并可访问这个url

Spring Security基础理论学习_第19张图片

拓展

数据表user的roles字段添加ROLE_前缀,但是配置类configure中hasRole("USER")方法没有使用前缀的原因:

查看hasRole处理逻辑,本身自带了ROLE_,将hasRole方法的参数拼接ROLE_然后和loadByUsername获取的user对象的authority进行匹配,如果匹配成功则该用户有权限放行;否则无权限显示403错误 (即存在可以登录但是无权限的情况)

// ExpressionUrlAuthorizationConfigurer

private static String hasRole(String role) {
    Assert.notNull(role, "role cannot be null");
    if (role.startsWith("ROLE_")) {
        throw new IllegalArgumentException("role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'");
    } else {
        return "hasRole('ROLE_" + role + "')";
    }
}

 

总结

模拟使用场景,模拟面试环节多问几个为什么,培养看源码的良好习惯,学习大佬们的编程风格

你可能感兴趣的:(后端开发,Spring,Security学习,java,spring,security)