window.cnblogsConfig = { blogUser: 'MoYu', blogAvatar: 'https://gitee.com/MoYu-zc/picgo/raw/master/img/20210213094450.jpg', blogStartDate: '2020-02-09', webpageTitleOnblur: '(o゚v゚)ノ Hi,Back', webpageTitleOnblurTimeOut: 500, webpageTitleFocus: '(*´∇`*) 欢迎回来!', webpageTitleFocusTimeOut: 1000, webpageIcon: "https://gitee.com/MoYu-zc/picgo/raw/master/img/20210213094450.jpg", enable: true, // 是否开启日/夜间模式切换按钮 auto: { // 自动切换相关配置 enable: false, // 开启自动切换 dayHour: 7, // 日间模式开始时间,整数型,24小时制 nightHour: 20 // 夜间模式开始时间,整数型,24小时制 } switchDayNight: { enable: true, auto: { enable: true } }, progressBar: { id : 'top-progress-bar', // 请勿修改该值 color : '#77b6ff', height : '2px', duration: 0.2, }, loading: { rebound: { tension: 16, friction: 5, }, spinner: { id: 'spinner', radius: 90, sides: 3, depth: 4, colors: { background: '#f0f0f0', stroke: '#272633', base: null, child: '#272633', }, alwaysForward: true, // When false the spring will reverse normally. restAt: 0.5, // A number from 0.1 to 0.9 || null for full rotation renderBase: false, } }, homeTopAnimationRendered: true, homeTopAnimation: { radius: 15, density: 0.2, color: 'rgba(255,255,255, .2)', // 颜色设置,“random” 为随机颜色 clearOffset: 0.3, }, essayTopAnimationRendered: true, essayTopAnimation: { triW : 14, triH : 20, neighbours : ["side", "top", "bottom"], speedTrailAppear : .1, speedTrailDisappear : .1, speedTriOpen : 1, trailMaxLength : 30, trailIntervalCreation : 100, delayBeforeDisappear : 2, colorsRandom: false, // v1.2.4 是否开启随机颜色 colors: [ '#96EDA6', '#5BC6A9', '#38668C', '#374D84', '#BED5CB', '#62ADC6', '#8EE5DE', '#304E7B' ] }, homeTopImg: [ "https://cdn.jsdelivr.net/gh/BNDong/Cnblogs-Theme-SimpleMemory@master/img/webp/home_top_bg.webp", "https://cdn.jsdelivr.net/gh/BNDong/Cnblogs-Theme-SimpleMemory@master/img/webp/home_top_bg.webp" ], homeBannerTextType: "one", essayTopImg: [ "https://cdn.jsdelivr.net/gh/BNDong/Cnblogs-Theme-SimpleMemory@master/img/webp/nothome_top_bg.webp", "https://cdn.jsdelivr.net/gh/BNDong/Cnblogs-Theme-SimpleMemory@master/img/webp/nothome_top_bg.webp", "https://gitee.com/MoYu-zc/picgo/raw/master/img/20210208190902.jpg", "https://gitee.com/MoYu-zc/picgo/raw/master/img/20210208190954.jpg", ], codeMaxHeight: true, codeLineNumber: true, essayCode: { fontFamily: "'Ubuntu Mono',monospace", // 代码框字体 fontSize: "14px" // 代码框字体大小 }, }

Spring Security 上

Spring Security 上

Security-dome

1.创建项目

创建一个Spring Boot项目,不用加入什么依赖

2.导入依赖

<dependencies>
    <!--启动器变为 web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--security启动器 -->
    <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>
        <scope>test</scope>
    </dependency>
</dependencies>

3.创建控制层

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

4.配置文件修改端口号

server.port=8081

5.运行测试

运行网址为:

http://localhost:8081/hello

这时候会发现,网址会自动变为:

http://localhost:8081/login

1

6.登录

能看到,在该页面中有账号密码

默认账号:user

默认密码:

2

登录之后:

3

Security 原理

Spring Security 本质是一个过滤器链

FilterSecurityInterceptor:是一个方法级的 权限过滤器 ,基本位于过滤链的最底部


ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常


UsernamePasswordAuthenticationFilter:对 /login 的POST请求做拦截,校验表单中用户名,密码


过滤器加载步骤

步骤流程

使用Spring Security配置过滤器 : DelegatingFilterProxy

源代码如下

public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    Filter delegateToUse = this.delegate;
    if (delegateToUse == null) {
        synchronized(this.delegateMonitor) {
            delegateToUse = this.delegate;
            if (delegateToUse == null) {
                WebApplicationContext wac = this.findWebApplicationContext();
                if (wac == null) {
                    throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?");
                }

                delegateToUse = this.initDelegate(wac);

            }
            this.delegate = delegateToUse;
        }
    }
    this.invokeDelegate(delegateToUse, request, response, filterChain);
}

即为:

public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //进行判断
        
        //初始化
		delegateToUse = this.initDelegate(wac);
		
    	//其余部分
}

然后我们查看 initDelegate

4

初始化为 FilterChainProxy 对象

进入 FilterChainProxy:

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
    if (!clearContext) {
        //满足条件  运行该方法
        this.doFilterInternal(request, response, chain);
        
    } else {
        try {
            request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
            //不满足  最终还是需要运行该方法
            this.doFilterInternal(request, response, chain);
            
        } catch (RequestRejectedException var9) {
            this.requestRejectedHandler.handle((HttpServletRequest)request, (HttpServletResponse)response, var9);
        } finally {
            SecurityContextHolder.clearContext();
            request.removeAttribute(FILTER_APPLIED);
        }
    }
}

可以看出,无论满不满足条件,最终都需要运行 doFilterInternal()方法

private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
     /*
    *  部分代码。。。
    */
    List<Filter> filters = this.getFilters((HttpServletRequest)firewallRequest);
    /*
    *  部分代码。。。
    */
    private List<Filter> getFilters(HttpServletRequest request) {
        int count = 0;
        Iterator var3 = this.filterChains.iterator();
        SecurityFilterChain chain;
        do {
            if (!var3.hasNext()) {
                return null;
            }
            chain = (SecurityFilterChain)var3.next();
            if (logger.isTraceEnabled()) {
                ++count;
                logger.trace(LogMessage.format("Trying to match request against %s (%d/%d)", chain, count, this.filterChains.size()));
            }
        } while(!chain.matches(request));
         //返回所有过滤器
        return chain.getFilters();
    }

所以 doFilterInternal() 方法 可以返回 所有要进行加载的过滤器


总结:

  1. 配置过滤器 DelegatingFilterProxy
  2. 在其中进行初始化 initDelegate
  3. 在初始化中得到 FilterChainProxy 对象
  4. 在其中运行的就是 doFilterInternal() 方法,该方法返回的就是 所有要进行加载的过滤器

UserDetailsService 接口

UserDetailsService接口 : 查询数据库用户名和密码过程

步骤:

  1. 创建类继承UsernamePasswordAuthenticationFilter,重写三个方法: attemptAuthentication() 、successfulAuthentication()、unsuccessfulAuthentication()
  2. 如果成功调用successfulAuthentication(),反之调用unsuccessfulAuthentication()
  3. 创建类实现UserDetailService,编写查询数据过程,返回User对象,这个User对象是安全框架提供对象

PasswordEncoder接口

PasswordEncoder接口 : 数据加密接口,用于返回User对象里面密码加密

加密方法:

BCryptPasswordEncoder是Spring Security官方推荐的密码解析器,平时多使用这个解析器。
BCryptPasswordEncoder是对bcrypt强散列方法的具体实现。是基于Hash算法实现的单向加密。可以通过strength控制加密强度,默认10.

BCryptPasswordEncoder b = new BCryptPasswordEncoder();
String zc = b.encode("zc");   //加密成功

Web权限

Security-dome 中可以看到,如果想要进入页面,还需要输入账号密码

而对于登陆时候的账号密码可以进行自定义设置

  1. 通过配置文件
  2. 通过配置类
  3. 自定义编写实现类

1.通过配置文件

spring.security.user.name=root
spring.security.user.password=root

这个时候再运行,会发现控制台不会出现密码,可以直接通过设置的账号密码登录

2.通过配置类

  1. 创建一个 SecurityConfig 配置类
  2. 重写configure()方法,注意看清参数,不要选错方法
  3. 很重要的一点:需要注入PasswordEncoder接口

如果不注入该接口,可能报 Encoded password does not look like BCrypt

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        String password = bCryptPasswordEncoder.encode("root");
        auth.inMemoryAuthentication()
                .withUser("root")  		//账号
                .password(password)     //加密的密码
                .roles("admin");        //权限
    }
   
    @Bean
    PasswordEncoder passwordEncoder(){
       return new BCryptPasswordEncoder();
    }
}

这时候,也可以直接使用你设置的账号密码登录页面

3.自定义编写实现类

  1. 编写userDetailsService实现类,返回User对象
  2. 创建一个 SecurityConfig 配置类

编写一个UserDetailsService实现类

在其中需要重写 loadUserByUsername() 方法,该方法用于登录

@Service("userDetailsService")
public class MyuserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        List<GrantedAuthority> auths =
                AuthorityUtils.commaSeparatedStringToAuthorityList("role");
        //返回的实际上是一个User对象,参数解析可以看下面
        return new User("root",
                new BCryptPasswordEncoder().encode("root"),auths);
    }
}

UserDetailsService 解析

对于该实现类中重写的 loadUserByUsername() 方法,返回的是 UserDetails 接口

5

在源代码中可以看出,实际上 UserDetails 接口,返回的是一个 User 对象

6

而在User对象中,需要返回三个参数:

String、String、Collection;

账号 、 密码 、集合(权限等信息)


创建一个 SecurityConfig 配置类

@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {

    @Autowired
    UserDetailsService userDetailsService;  // 这里应和 @Service("userDetailsService") 中内容相同

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //使用该方法
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

这时候测试,也可以直接使用设置的账号密码登录

之后,如果连接数据库,一般都是用第三种方式

4.连接数据库完成用户认证

(该方法是在第三种方法代码基础上完成)

  1. 创建数据库
  2. 整合Mybatis-Plus完成数据库操作
  3. 配置JDBC信息
  4. 创建实体类、Mapper接口
  5. 创建UserDetailsService类

创建数据库

创建了一个 mybatis-plus 数据库 ,其中创建了一个users表,记得创建后,加入数据

7

引入依赖

<!-- Mybatis-plus-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.1</version>
</dependency>
<!-- mysql-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

配置JDBC信息

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis-plus?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root

创建实体类、Mapper接口

@Data    // 引入了Lombok才可以使用
public class Users {
    private Integer id;
    private String username;
    private String password;
}
@Repository
@Mapper
public interface UsersMapper extends BaseMapper<Users> {
}

创建UserDetailsService类

@Service("userDetailsService")
public class MyuserDetailsService implements UserDetailsService {
    @Autowired
    private UsersMapper usersMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //调用usersMapper方法
        QueryWrapper<Users> wrapper = new QueryWrapper<>();
        wrapper.eq("username",username);
        Users users = usersMapper.selectOne(wrapper);
        if (users == null){
            //数据库没有用户名,认证失败
            throw new UsernameNotFoundException("用户名不存在");
        }
        List<GrantedAuthority> auths =
                AuthorityUtils.commaSeparatedStringToAuthorityList("role");
        return new User(users.getUsername(),
                new BCryptPasswordEncoder().encode(users.getPassword()),auths);
    }
}

这时候就可以正常运行了

5.自定义登录页面

在上面代码的基础上完成该部分代码

1.创建前端页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form method="post" action="/user/login">
        用户名:<input type="text" name="username">
        <br>
        密码:<input type="text" name="password">
        <br>
         <input type="submit" value="login">
    </form>
</body>
</html>

2.书写Controller层代码

@GetMapping("/index")
public String index(){
    return "index";
}

3.在创建的配置类中重写 configure(HttpSecurity http) 方法

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin()   //自定义自己编写的登录页面
        .loginPage("/login.html")  //设置登陆页面
        .loginProcessingUrl("/login") //成功登录访问路径 , 该处路径和from表单中的action路径统一
        .defaultSuccessUrl("/index").permitAll()  //登录成功之后跳转路径
        .and().authorizeRequests()
        .antMatchers("/","/hello","/login") //可以直接访问的路径,不需要认证
        .permitAll()
        .anyRequest().authenticated()
        .and().csrf().disable(); //关闭csrf
}

这时候可以分别测试进入以下两个路径:

http://localhost:8081/hello

http://localhost:8081/index

会发现,第一个 hello 路径 ,不会拦截了,可以直接进入页面

第二个index,会进入自定义的登陆页面,登陆成功后,才可以进入

基于角色或权限的访问控制

1.hasAuthority方法

如果当前的主体具有指定的权限,则返回true,否则返回false

  1. 修改配置类
  2. 在 UserDetailsService 实现类中添加权限
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin() 
        .loginPage("/login.html") 
        .loginProcessingUrl("/user/login") 
        .defaultSuccessUrl("/index").permitAll()  
        .and().authorizeRequests()
        .antMatchers("/","/hello","/user/login") 
        .permitAll()
        
        //当前登录用户,只有具有admins权限才可以访问这个路径
        .antMatchers("/index").hasAuthority("admins")
        
        .anyRequest().authenticated()
        .and().csrf().disable(); 
}
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<Users> wrapper = new QueryWrapper<>();
        wrapper.eq("username",username);
        Users users = usersMapper.selectOne(wrapper);
        if (users == null){ 
            throw new UsernameNotFoundException("用户名不存在");
        }
        
        List<GrantedAuthority> auths =
                AuthorityUtils.commaSeparatedStringToAuthorityList("admins");    //这里添加权限
        return new User(users.getUsername(),
                new BCryptPasswordEncoder().encode(users.getPassword()),auths);
    }

进行测试,路径为:

http://localhost:8081/index

  • 如果权限不通过 , 403 无权限

8

  • 如果权限通过 ,正常运行

2.hasAnyAuthority方法

如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回 true

与 hasAuthority() 的区别是

  • hasAuthority() 参数唯一,只能满足这一个权限才可以

  • 而该方法,参数可以多个,满足其中一个权限 即为通过

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin() 
        .loginPage("/login.html") 
        .loginProcessingUrl("/user/login") 
        .defaultSuccessUrl("/index").permitAll()  
        .and().authorizeRequests()
        .antMatchers("/","/hello","/user/login") 
        .permitAll()

        //当前登录用户,具有admins或者user权限才可以访问这个路径
        .antMatchers("/index").hasAnyAuthority("admins","user")

        .anyRequest().authenticated()
        .and().csrf().disable(); 
}

3.hasRole方法

如果用户具备给定角色就 允许访问,否则出现 403
如果当前主体具有指定的角色,则返回 true

该方法与 hasAuthority 方法,使用方法基本相同,区别就是 他需要在权限前加上 ROLE_

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin() 
        .loginPage("/login.html") 
        .loginProcessingUrl("/user/login") 
        .defaultSuccessUrl("/index").permitAll()  
        .and().authorizeRequests()
        .antMatchers("/","/hello","/user/login") 
        .permitAll()

        //当前登录用户,只有具有 ROLE_user 权限才可以访问这个路径
        .antMatchers("/index").hasRole("user")

        .anyRequest().authenticated()
        .and().csrf().disable(); 
}
// UserDetailsService 实现类中添加权限
List<GrantedAuthority> auths =
                AuthorityUtils.commaSeparatedStringToAuthorityList("admins,ROLE_user");

这时候可以正常运行

4.hasAnyRole方法

表示用户具备任何一个条件都可以访问

该方法与 hasRole() 的区别 与1 2 两种方法相同,大家可以自行测试

5.自定义403页面

1.创建自定义403页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
	<h1>没有权限访问!!!</h1>
</body>
</html>

2.修改配置类

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.exceptionHandling().accessDeniedPage("/uuauth.html");
}

9

个人博客为:
MoYu's Github Blog
MoYu's Gitee Blog

posted @ 2021-05-06 20:29  MoYu-zc  阅读(373)  评论(0编辑  收藏  举报