SpringSecurity基础知识总结

Lou.Chen2022年7月8日
大约 30 分钟

1、Spring Security简介

Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直是 Shiro 的天下。

相对于 Shiro,在 SSM/SSH 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有 Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。

自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了 自动化配置方案,可以零配置使用 Spring Security。

因此,一般来说,常见的安全管理技术栈的组合是这样的:

  • SSM + Shiro
  • Spring Boot/Spring Cloud + Spring Security

注意,这只是一个推荐的组合而已,如果单纯从技术上来说,无论怎么组合,都是可以运行的。

2、SpringSecurity初体验

  • pom.xml
<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.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
</dependencies>
  • UserController
@RestController
public class UserController {
    @GetMapping("/hello")
    public String hello(){
        return "hello lc";
    }
}

2.1 默认进入security登录页面

在SpringSecurity中,已经对所有接口静态资源进行保护。

所以首次请求时需要用户名密码,密码由springSecurity动态生成。在控制台打印,默认为:用户名: user 密码: Using generated security password: 14404948-b30c-4cf3-8d02-cbb79779ffab

2.2 手动配置用户名密码

方式1:yaml配置用户名密码

spring:
  security:
    user:
#      配置用户名
      name: lc
#      配置密码
      password: 123456
#      配置角色
      roles: admin

方式2:配置文件配置用户名密码, 继承WebSecurityConfigurerAdapter

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

//    spring5开始 security中使用配置文件的设置用户密码的必须要加密
//    所有这里我们设置密码不加密
//    配置密码编码器
    @Bean
    PasswordEncoder passwordEncoder() {
//        此方法已过期(这里我们只是测试实验)
        return NoOpPasswordEncoder.getInstance();
    }

//    认证管理
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//      在内存中的认证
        auth.inMemoryAuthentication()
//                设置多个用户名,密码,角色
                .withUser("lc").password("123456").roles("admin")
                .and()
                .withUser("zs").password("123").roles("user");
    }
}

3、HttpSecurity详解规则

  • UserController
@RestController
public class UserController {
    @GetMapping("/hello")
    public String hello(){
        return "hello lc";
    }
    @GetMapping("/admin/hello")
    public String admin(){
        return "hello admin";
    }
    @GetMapping("/user/hello")
    public String user(){
        return "hello user";
    }
}

  • SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


//    spring5开始 security中使用配置文件的设置用户密码的必须要加密
//    所有这里我们设置密码不加密
//    配置密码编码器
    @Bean
    PasswordEncoder passwordEncoder() {
//        此方法已过期(这里我们只是测试实验)
        return NoOpPasswordEncoder.getInstance();
    }

//    认证管理
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//      在内存中的认证
        auth.inMemoryAuthentication()
//                设置多个用户名,密码,角色
                .withUser("lc").password("123456").roles("admin")
                .and()
                .withUser("zs").password("123").roles("user");
    }

//	路径配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        开启授权请求
        http.authorizeRequests()
//                路径匹配  满足/admin/**路径的 必须要有admin角色
                .antMatchers("/admin/**").hasRole("admin")
//                路径匹配  满足/user/**路径的 必须要有其中的一个角色即可访问
//                .antMatchers("/user/**").hasAnyRole("admin","user")
//                另一种写法==> 必须要有其中的一个角色即可访问
                .antMatchers("/user/**").access("hasAnyRole('admin','user')")
//                路径匹配 满足/manager/**路径规则的 必须要同时拥有admin和manager角色
                .antMatchers("/manager/**").access("hasRole('admin') and hasRole('manager')")
//                其他的请求只要认证即可访问
                .anyRequest().authenticated()
                .and()
//                开启表单登录
                .formLogin()
                //处理表单登录的路径
                //通过该路径登录的用户名和密码再去请求接口就不会有登录页面的出现,若直接走其他接口则会进入登录页面
                //注意这里为post请求
                .loginProcessingUrl("/doLogin")
//                跟登录有关的接口 直接运行访问
                .permitAll()
                .and()
//                关闭crsf攻击(跨站请求伪造),便于我们测试
                .csrf().disable();
    }
}
2、登录表单的详细配置
①、Usercontroller
@RestController
public class UserController {

//    登录即可访问
    @GetMapping("/hello")
    public String hello(){
        return "hello lc";
    }

//    模拟admin接口
    @GetMapping("/admin/hello")
    public String admin(){
        return "hello admin";
    }

//    模拟user接口
    @GetMapping("/user/hello")
    public String user(){
        return "hello user";
    }

  
}
②、LoginController
//若前后端不分离,设置默认登录成功的路径 我们只需要跳转到主页连接即可(注意:这里的请求只支持post)
//                .successForwardUrl("/index111")
//                若前后端不分离 设置默认登录成功的路径 我们只需要跳转到主页链接即可(注意:这里的请求只支持get)
//                .defaultSuccessUrl("/index111",true)
@Controller
public class LoginController {

    //模拟登录成功后跳转的页面
    @GetMapping("/index111")
    public String toIndex() {
        return "index11";
    }

//    模拟登录的页面
    @GetMapping("/login")
    public String logon() {
        return "login";
    }
}
③、登录页login.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>登录</h1>

<form action="/doLogin" method="post">
    <div> 用户名:<input type="text" name="uname" placeholder="请输入用户名"></div>
    <div> 密码:<input type="text" name="passwd" placeholder="请输入密码"></div>
    <div><input type="submit" value="登录"></div>
</form>
</body>
</html>
④、主页index11.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>主页!!!!</h1>
</body>
</html>
③、SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


//    spring5开始 security中使用配置文件的设置用户密码的必须要加密
//    所有这里我们设置密码不加密
//    配置密码编码器
    @Bean
    PasswordEncoder passwordEncoder() {
//        此方法已过期(这里我们只是测试实验)
        return NoOpPasswordEncoder.getInstance();
    }

//    认证管理
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//      在内存中的认证
        auth.inMemoryAuthentication()
//                设置多个用户名,密码,角色
                .withUser("lc").password("123456").roles("admin")
                .and()
                .withUser("zs").password("123").roles("user");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        开启授权请求
        http.authorizeRequests()
//                路径匹配  满足/admin/**路径的 必须要有admin角色
                .antMatchers("/admin/**").hasRole("admin")
//                路径匹配  满足/user/**路径的 必须要有其中的一个角色即可访问
//                .antMatchers("/user/**").hasAnyRole("admin","user")
//                另一种写法==> 必须要有其中的一个角色即可访问
                .antMatchers("/user/**").access("hasAnyRole('admin','user')")
//                路径匹配 满足/manager/**路径规则的 必须要同时拥有admin和manager角色
                .antMatchers("/manager/**").access("hasRole('admin') and hasRole('manager')")
//                其他的请求只要认证即可访问
                .anyRequest().authenticated()
                .and()
//                开启表单登录
                .formLogin()
                //处理表单登录的路径
                //通过该路径登录的用户名和密码再去请求接口就不会有登录页面的出现,若直接走其他接口则会进入登录页面
                //注意这里为post请求
                .loginProcessingUrl("/doLogin")
//                自定义跳转到的登录页的路径(默认登录页失效)
//                在前后端分离的情况下 我们只需要返回json即可
                .loginPage("/login")
//                设置自定义的登录的用户名的key的名称
                .usernameParameter("uname")
//                设置自定义的登录的密码的key的名称
                .passwordParameter("passwd")
//                若前后端不分离,设置默认登录成功的路径 我们只需要跳转到主页连接即可(注意:这里的请求只支持post)
//                .successForwardUrl("/index111")
//                若前后端不分离 设置默认登录成功的路径 我们只需要跳转到主页链接即可(注意:这里的请求只支持get)
//                .defaultSuccessUrl("/index111",true)
//                登录成功的的自定义处理
//                若前后端分离 则我们只需要处理是否登录成功返回相应的json即可
                .successHandler(new AuthenticationSuccessHandler() {

//                    Authentication用户登录的信息
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse res, Authentication authentication) throws IOException, ServletException {
//                        设置响应头类型
                        res.setContentType("application/json;charset=utf-8");
//                        获取打印输出流
                        PrintWriter printWriter=res.getWriter();
//                        响应自定义的数据
                        Map<String,Object> map=new HashMap<>();
//                        自定义状态码
                        map.put("status", 200);
//                        登录的信息
//                        map.put("msg", authentication.getPrincipal());
                        //"msg": {
                        //        "password": null,
                        //        "username": "lc",
                        //        "authorities": [
                        //            {
                        //                "authority": "ROLE_admin"
                        //            }
                        //        ],
                        //        "accountNonExpired": true,
                        //        "accountNonLocked": true,
                        //        "credentialsNonExpired": true,
                        //        "enabled": true
                        //    },
//                        map.put("msg1", authentication.getDetails());
                        // "msg1": {
                        //        "remoteAddress": "0:0:0:0:0:0:0:1",
                        //        "sessionId": null
                        //    },
//                        map.put("msg2", authentication.getAuthorities());
                        // "msg2": [
                        //        {
                        //            "authority": "ROLE_admin"
                        //        }
                        //    ],
//                        map.put("msg3", authentication.getCredentials());
                        //"msg3": null,
//                        map.put("msg4", authentication.isAuthenticated());
                        //"msg4": true,
//                        将map转为json字符串
                        printWriter.write(new ObjectMapper().writeValueAsString(map));
                        printWriter.flush();
                        printWriter.close();
                    }
                })
//               同理,前后端不分离进入登录失败的处理路径(支持post)
//                .failureForwardUrl("/error")
//                前后端分离 进入登录失败的自定义处理
                .failureHandler(new AuthenticationFailureHandler() {
//                    AuthenticationException 验证异常的信息
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse res, AuthenticationException e) throws IOException, ServletException {
//                        设置响应头类型
                        res.setContentType("application/json;charset=utf-8");
//                        获取打印输出流
                        PrintWriter printWriter=res.getWriter();
//                        响应自定义的数据
                        Map<String,Object> map=new HashMap<>();
//                        自定义状态码
                        map.put("status", 401);
//                        找到错误的类型 设置错误信息
//                        Credentials 证书 凭证
                        if(e instanceof LockedException){
                            map.put("msg", "账户被锁定,登录失败");
                        }else if(e instanceof BadCredentialsException){
                            map.put("msg", "用户名或密码输入错误,登录失败!");
                        }else if(e instanceof DisabledException){
                            map.put("msg", "账号被禁用,登录失败!");
                        }else if(e instanceof AccountExpiredException){
                            map.put("msg", "账户过期,登录失败!");
                        }else if(e instanceof CredentialsExpiredException){
                            map.put("msg", "密码过期,登录失败!");
                        }else{
                            map.put("msg", "登录失败!");
                        }
//                        将map转为json字符串
                        printWriter.write(new ObjectMapper().writeValueAsString(map));
                        printWriter.flush();
                        printWriter.close();
                    }
                })
//                跟登录有关的接口 直接运行访问
                .permitAll()
                .and()
//                关闭crsf攻击(跨站请求伪造),便于我们测试
                .csrf().disable();
    }
}
3、注销登录配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


//    spring5开始 security中使用配置文件的设置用户密码的必须要加密
//    所有这里我们设置密码不加密
//    配置密码编码器
    @Bean
    PasswordEncoder passwordEncoder() {
//        此方法已过期(这里我们只是测试实验)
        return NoOpPasswordEncoder.getInstance();
    }

//    认证管理
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//      在内存中的认证
        auth.inMemoryAuthentication()
//                设置多个用户名,密码,角色
                .withUser("lc").password("123456").roles("admin")
                .and()
                .withUser("zs").password("123").roles("user");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        开启授权请求
        http.authorizeRequests()
//                路径匹配  满足/admin/**路径的 必须要有admin角色
                .antMatchers("/admin/**").hasRole("admin")
//                路径匹配  满足/user/**路径的 必须要有其中的一个角色即可访问
//                .antMatchers("/user/**").hasAnyRole("admin","user")
//                另一种写法==> 必须要有其中的一个角色即可访问
                .antMatchers("/user/**").access("hasAnyRole('admin','user')")
//                路径匹配 满足/manager/**路径规则的 必须要同时拥有admin和manager角色
                .antMatchers("/manager/**").access("hasRole('admin') and hasRole('manager')")
//                其他的请求只要认证即可访问
                .anyRequest().authenticated()
                .and()
//                开启表单登录
                .formLogin()
                //处理表单登录的路径
                //通过该路径登录的用户名和密码再去请求接口就不会有登录页面的出现,若直接走其他接口则会进入登录页面
                //注意这里为post请求
                .loginProcessingUrl("/doLogin")
//                自定义跳转到的登录页的路径(默认登录页失效)
//                在前后端分离的情况下 我们只需要返回json即可
                .loginPage("/login")
//                设置自定义的登录的用户名的key的名称
                .usernameParameter("uname")
//                设置自定义的登录的密码的key的名称
                .passwordParameter("passwd")
//                若前后端不分离,设置默认登录成功的路径 我们只需要跳转到主页连接即可(注意:这里的请求只支持post)
//                .successForwardUrl("/index111")
//                若前后端不分离 设置默认登录成功的路径 我们只需要跳转到主页链接即可(注意:这里的请求只支持get)
//                .defaultSuccessUrl("/index111",true)
//                登录成功的的自定义处理
//                若前后端分离 则我们只需要处理是否登录成功返回相应的json即可
                .successHandler(new AuthenticationSuccessHandler() {

//                    Authentication报错用户登录的信息
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse res, Authentication authentication) throws IOException, ServletException {
//                        设置响应头类型
                        res.setContentType("application/json;charset=utf-8");
//                        获取打印输出流
                        PrintWriter printWriter=res.getWriter();
//                        响应自定义的数据
                        Map<String,Object> map=new HashMap<>();
//                        自定义状态码
                        map.put("status", 200);
//                        登录的信息
                        map.put("msg", authentication.getPrincipal());
                        //"msg": {
                        //        "password": null,
                        //        "username": "lc",
                        //        "authorities": [
                        //            {
                        //                "authority": "ROLE_admin"
                        //            }
                        //        ],
                        //        "accountNonExpired": true,
                        //        "accountNonLocked": true,
                        //        "credentialsNonExpired": true,
                        //        "enabled": true
                        //    },
//                        map.put("msg1", authentication.getDetails());
                        // "msg1": {
                        //        "remoteAddress": "0:0:0:0:0:0:0:1",
                        //        "sessionId": null
                        //    },
//                        map.put("msg2", authentication.getAuthorities());
                        // "msg2": [
                        //        {
                        //            "authority": "ROLE_admin"
                        //        }
                        //    ],
//                        map.put("msg3", authentication.getCredentials());
                        //"msg3": null,
//                        map.put("msg4", authentication.isAuthenticated());
                        //"msg4": true,
//                        将map转为json字符串
                        printWriter.write(new ObjectMapper().writeValueAsString(map));
                        printWriter.flush();
                        printWriter.close();
                    }
                })
//               同理,前后端不分离进入登录失败的处理路径(支持post)
//                .failureForwardUrl("/error")
//                前后端分离 进入登录失败的自定义处理
                .failureHandler(new AuthenticationFailureHandler() {
//                    AuthenticationException 验证异常的信息
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse res, AuthenticationException e) throws IOException, ServletException {
//                        设置响应头类型
                        res.setContentType("application/json;charset=utf-8");
//                        获取打印输出流
                        PrintWriter printWriter=res.getWriter();
//                        响应自定义的数据
                        Map<String,Object> map=new HashMap<>();
//                        自定义状态码
                        map.put("status", 401);
//                        找到错误的类型 设置错误信息
//                        Credentials 证书 凭证
                        if(e instanceof LockedException){
                            map.put("msg", "账户被锁定,登录失败");
                        }else if(e instanceof BadCredentialsException){
                            map.put("msg", "用户名或密码输入错误,登录失败!");
                        }else if(e instanceof DisabledException){
                            map.put("msg", "账号被禁用,登录失败!");
                        }else if(e instanceof AccountExpiredException){
                            map.put("msg", "账户过期,登录失败!");
                        }else if(e instanceof CredentialsExpiredException){
                            map.put("msg", "密码过期,登录失败!");
                        }else{
                            map.put("msg", "登录失败!");
                        }
//                        将map转为json字符串
                        printWriter.write(new ObjectMapper().writeValueAsString(map));
                        printWriter.flush();
                        printWriter.close();
                    }
                })
//                跟登录有关的接口 直接运行访问
                .permitAll()
                .and()
                .logout()
//                定义注销的路径
                .logoutUrl("/logout")
//                自定义注销的处理
                .logoutSuccessHandler(new LogoutSuccessHandler() {
//                    authentication 登录用户的信息
                    @Override
                    public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse res, Authentication authentication) throws IOException, ServletException {
                        //                        设置响应头类型
                        res.setContentType("application/json;charset=utf-8");
//                        获取打印输出流
                        PrintWriter printWriter=res.getWriter();
//                        响应自定义的数据
                        Map<String,Object> map=new HashMap<>();
//                        自定义状态码
                        map.put("status", 200);
                        map.put("msg", "注销登录成功!");
//                        将map转为json字符串
                        printWriter.write(new ObjectMapper().writeValueAsString(map));
                        printWriter.flush();
                        printWriter.close();
                    }
                })
                .and()
//                关闭crsf攻击(跨站请求伪造),便于我们测试
                .csrf().disable();
    }
}

三、多个httpSecurity配置

1、controller
@RestController
public class UserController {

//    登录即可访问
    @GetMapping("/hello")
    public String hello(){
        return "hello lc";
    }

//    模拟admin接口
    @GetMapping("/admin/hello")
    public String admin(){
        return "hello admin";
    }

//    模拟user接口
    @GetMapping("/user/hello")
    public String user(){
        return "hello user";
    }
}

2、SecurityConfig

注意: httpSecurity.antMatcher("/admin/**").authorizeRequests().anyRequest().hasRole("admin")

*只是对/admin/*的形式的路径进行拦截,而不是所有路径

@Configuration
public class MultipleHttpSecurity {

    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    /**
     * 认证管理
     * @param auth  这里相当于注入 AuthenticationManagerBuilder对象
     * @throws Exception
     */
    @Autowired
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//      在内存中的认证
        auth.inMemoryAuthentication()
//                设置多个用户名,密码,角色
                .withUser("lc").password("111").roles("admin")
                .and()
                .withUser("zs").password("222").roles("user");
    }

    @Configuration
//    数字越小优先级越高
    @Order(1)
    public static class AdminSecurityConfig extends WebSecurityConfigurerAdapter{
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {
//          表示当前httpsecurity只拦截 /admin/** 路径,并且需要admin角色
            httpSecurity.antMatcher("/admin/**").authorizeRequests().anyRequest().hasRole("admin");
        }
    }

    @Configuration
    public static class OtherSecurityConfig extends WebSecurityConfigurerAdapter{
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {
//            开启授权请求,所有的请求都需要认证即可访问
//            这里经过上面的admin的请求后 再拦截其他的所有请求
            httpSecurity.authorizeRequests().anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .loginProcessingUrl("/doLogin")
                    .permitAll()
                    .and()
                    .csrf().disable();
        }
    }
    
}

四、BCryptPasswordEncoder密码加密

​ spring security中的BCryptPasswordEncoder方法采用SHA-256 +随机盐+密钥对密码进行加密。SHA系列是Hash算法,不是加密算法,使用加密算法意味着可以解密(这个与编码/解码一样),但是采用Hash处理,其过程是不可逆的。

**(1)加密(encode):**注册用户时,使用SHA-256+随机盐+密钥把用户输入的密码进行hash处理,得到密码的hash值,然后将其存入数据库中。

**(2)密码匹配(matches):**用户登录时,密码匹配阶段并没有进行密码解密(因为密码经过Hash处理,是不可逆的),而是使用相同的算法把用户输入的密码进行hash处理,得到密码的hash值,然后将其与从数据库中查询到的密码hash值进行比较。如果两者相同,说明用户输入的密码正确。

这正是为什么处理密码时要用hash算法,而不用加密算法。因为这样处理即使数据库泄漏,黑客也很难破解密码(破解密码只能用彩虹表)。

@SpringBootTest
class JpaRestApplicationTests {

    @Test
    void Test1(){
//        可定义循环迭代的强度(默认为10)
        BCryptPasswordEncoder bCryptPasswordEncoder=new BCryptPasswordEncoder();
        String encode="";
        for (int i = 0; i < 10; i++) {
            //加密
            encode = bCryptPasswordEncoder.encode("111");
            System.out.println(encode);
        }
//        解密
//        验证加密的和原密码是否匹配
        boolean matches = bCryptPasswordEncoder.matches("111", encode);
        System.out.println(matches);
        
//        我们可以看到每次加密的密码都不一致
        //$2a$10$4f0bp6uBiLdt2mfZcL4ITuolV7C1wNSAvmC5DCkQQ/wIbf0N4aiYu
        //$2a$10$3J7jUw/4d5tIJc.36e/SMOf3g3SWjFwYnBnc5q90F9jsHkbbfrbWO
        //$2a$10$oDbDEQonfNF3VccU9rw2u.PCEYgPz7sdEA/Zk3vlhq7nnA6zFptUq
        //$2a$10$h5mGJ1JleyKqJ0cXThgiv.D6tXWtsJovWCGn3lRC2VKzyHWBrrnSS
        //$2a$10$005mvGASGZVRgNzm2upYsOXTCgwZPhxhHsJQM50DFE9PTW0GR3hY.
        //$2a$10$gVbxdtiMstQESB0/EBbZq.4rPWHpgq20AUK.Jlq4xUE.g3jPGa5Mq
        //$2a$10$HWUDxz8SZYorijp05yeJH.JdkF8Jm.y2/rVnAXEXObprxeBWZYRfa
        //$2a$10$rpslaqYmHY9VCXY6U94nNO69rzwU34vDDFudW/APYFgLb3oVeVNu6
        //$2a$10$7o41f4IV/uNwPmDKd4Rl/ehdPY5FT5uCvt1aofXwqwWTdkOQK/9L.
        //$2a$10$6NfXSvvKtzpgdjskRhoiMeqq9xTaWsCS7FdDr/I1I3Iy4IeT0tuta

    }
修改securityconfig配置
@Configuration
public class MultipleHttpSecurity {

//    这里我们使用BCryptPasswordEncoder加密
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 认证管理
     * @param auth  这里相当于注入 AuthenticationManagerBuilder对象
     * @throws Exception
     */
    @Autowired
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//      在内存中的认证
        auth.inMemoryAuthentication()
//                设置多个用户名,密码,角色
                .withUser("lc").password("111").roles("admin")
                .and()
                .withUser("zs").password("$2a$10$4f0bp6uBiLdt2mfZcL4ITuolV7C1wNSAvmC5DCkQQ/wIbf0N4aiYu").roles("user");
    }

    @Configuration
//    数字越小优先级越高
    @Order(1)
    public static class AdminSecurityConfig extends WebSecurityConfigurerAdapter{
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {
//          表示当前httpsecurity只拦截 /admin/** 路径,并且需要admin角色
            httpSecurity.antMatcher("/admin/**").authorizeRequests().anyRequest().hasRole("admin");
        }
    }

    @Configuration
    public static class OtherSecurityConfig extends WebSecurityConfigurerAdapter{
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {
//            开启授权请求,所有的请求都需要认证即可访问
//            这里经过上面的admin的请求后 再拦截其他的所有请求
            httpSecurity.authorizeRequests().anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .loginProcessingUrl("/doLogin")
                    .permitAll()
                    .and()
                    .csrf().disable();
        }
    }

}

五、方法安全管控

1、注解详解

​ spring security默认禁用注解,要想开启注解,需要在继承WebSecurityConfigurerAdapter的类上加**@EnableGlobalMethodSecurity**注解,并在该类中将AuthenticationManager定义为bean。

这里springSecurity默认禁用所有方法保护的注解 @EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)

prePostEnabled开启 @PreAuthorize@PostAuthorize 注解 securedEnabled 开启@Secured注解

@EnableGlobalMethodSecurity启用注解
1、@PreAuthorize

在方法执行前判断,可以调用方法参数,主要利用Java8的参数名反射特性,如果没用Java8也可以使用spring security的@P标注参数,或者Spring Data的@Param标注参数。

//判断用户是否为当前登录用户或拥有ROLE_ADMIN权限
@PreAuthorize("#userId == authentication.principal.userId or hasAuthority(‘ADMIN’)")
public void changePassword(@P("userId") long userId ){}
2、@PostAuthorize

在方法执行后判断,可以调用参数。如果EL为false,虽然方法已经执行完了也可能会回滚,EL变量returnObject表示返回的对象。

3、@Secured

只支持字符串形式,且必须要加上前缀ROLE_

是否有权限访问

2、基本配置
①、securityConfig
@Configuration
//这里springSecurity默认禁用所有方法保护的注解
//prePostEnabled开启 @PreAuthorize和@PostAuthorize 注解
//securedEnabled 开启@Secured注解
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class MultipleHttpSecurity {

//    这里我们使用BCryptPasswordEncoder加密
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 认证管理
     * @param auth  这里相当于注入 AuthenticationManagerBuilder对象
     * @throws Exception
     */
    @Autowired
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//      在内存中的认证
        auth.inMemoryAuthentication()
//                设置多个用户名,密码,角色
                .withUser("lc").password("$2a$10$4f0bp6uBiLdt2mfZcL4ITuolV7C1wNSAvmC5DCkQQ/wIbf0N4aiYu").roles("admin")
                .and()
                .withUser("zs").password("$2a$10$4f0bp6uBiLdt2mfZcL4ITuolV7C1wNSAvmC5DCkQQ/wIbf0N4aiYu").roles("user");
    }

    @Configuration
//    数字越小优先级越高
    @Order(1)
    public static class AdminSecurityConfig extends WebSecurityConfigurerAdapter{
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {
//          表示当前httpsecurity只拦截 /admin/** 路径,并且需要admin角色
            httpSecurity.antMatcher("/admin/**").authorizeRequests().anyRequest().hasRole("admin");
        }
    }

    @Configuration
    public static class OtherSecurityConfig extends WebSecurityConfigurerAdapter{
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {
//            开启授权请求,所有的请求都需要认证即可访问
//            这里经过上面的admin的请求后 再拦截其他的所有请求
            httpSecurity.authorizeRequests().anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .loginProcessingUrl("/doLogin")
                    .permitAll()
                    .and()
                    .csrf().disable();
        }
    }

}

②、userService
@Service
public class UserService {

    /**
     * 方法进入前 验证是否有admin角色
     * @return
     */
    @PreAuthorize("hasRole('admin')")
    //同时要有admin和user角色
//    @PreAuthorize("hasRole('admin') and hasRole('user')")
    public String admin() {
        return "hello admin";
    }

    /**
     * 是否有user角色
     * @return
     */
    @Secured("ROLE_user")
    public String user() {
        return "hello user";
    }

    /**
     * 方法进入前 验证是否有admin或者user角色
     * @return
     */
    @PreAuthorize("hasAnyRole('admin','user')")
    public String hello(){
        return "hello evenyone";
    }

}

③、controller
@RestController
public class UserController {
//    登录即可访问
    @GetMapping("/hello")
    public String hello(){
        return "hello lc";
    }
//    模拟admin接口
    @GetMapping("/admin/hello")
    public String admin(){
        return "hello admin";
    }
//    模拟user上接口
    @GetMapping("/user/hello")
    public String user(){
        return "hello user";
    }
}

六、基于数据库的认证

1、sql
SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) DEFAULT NULL,
  `nameZh` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES ('1', 'dba', '数据库管理员');
INSERT INTO `role` VALUES ('2', 'admin', '系统管理员');
INSERT INTO `role` VALUES ('3', 'user', '用户');

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `enabled` tinyint(1) DEFAULT NULL,
  `locked` tinyint(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user 密码123
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'root', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
INSERT INTO `user` VALUES ('2', 'admin', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
INSERT INTO `user` VALUES ('3', 'sang', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');

-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `uid` int(11) DEFAULT NULL,
  `rid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES ('1', '1', '1');
INSERT INTO `user_role` VALUES ('2', '1', '2');
INSERT INTO `user_role` VALUES ('3', '2', '2');
INSERT INTO `user_role` VALUES ('4', '3', '3');
SET FOREIGN_KEY_CHECKS=1;
2、pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.5.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.lc</groupId>
	<artifactId>security-db</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>security-db</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.1.2</version>
		</dependency>

		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid-spring-boot-starter</artifactId>
			<version>1.1.18</version>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.27</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<resources>
			<resource>
				<directory>src/main/java</directory>
				<includes>
					<include>**/*.xml</include>
				</includes>
			</resource>
			<resource>
				<directory>src/main/resources</directory>
			</resource>
		</resources>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

3、实体

这里提供的UserDetails只是一种规范

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

①user

public class User implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    /**
     * 是否启用
     */
    private Boolean enabled;
    /**
     * 是否锁定
     */
    private Boolean locked;

    /**
     * 一个用户可以包含多个角色
     */
    private List<Role> roles;

    public Integer getId() {
        return id;
    }

    public User setId(Integer id) {
        this.id = id;
        return this;
    }

    public User setUsername(String username) {
        this.username = username;
        return this;
    }

    public User setPassword(String password) {
        this.password = password;
        return this;
    }

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

    public User setLocked(Boolean locked) {
        this.locked = locked;
        return this;
    }

    public List<Role> getRoles() {
        return roles;
    }

    public User setRoles(List<Role> roles) {
        this.roles = roles;
        return this;
    }

    @Override
    public String getPassword(){
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    /**
     * 返回用户的所有角色
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities=new ArrayList<>();
//        将数据中查询的角色信息查出并传入springsecurity
        for (Role role : roles) {
//            注意若数据中的角色名没有以ROLE_开头,则需要在这动态赋予。否则则不需要
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
        }
        return authorities;
    }

    /**
     * 账户是否没有未过期
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 账户是否未锁定 相当于locked的get方法
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        //这里的数据库中为0,则这里的locked为false; 因为这里问是否未被锁定,所以这里要取反
        return !locked;
    }

    /**
     * 凭证(密码)是否未过期
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 是否可用  相当于enabled的get方法
     * @return
     */
    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

②role

@Setter
@Getter
@ToString
public class Role {
    private Integer id;
    private String name;
    private String nameZh;
}
5、mapper

①接口

public interface UserMapper {

    User loadUserByUsername(String username);

    //根据用户id查询所有该用户对应的角色
    List<Role> getUserRolesById(Integer id);
}

②xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.lc.securitydb.mapper.UserMapper">

    <select id="loadUserByUsername" resultType="com.lc.securitydb.bean.User">
        select * from user where username=#{username};
    </select>

    <select id="getUserRolesById" resultType="com.lc.securitydb.bean.Role">
      select * from role where id in(select rid from user_role where uid=#{id})
    </select>

</mapper>
6、service

同理,这里我们提供UserDetailsService是一种规范

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
@Service
public class UserService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 根据用户名查询用户  这里我们只需要根据用户名查出即可 后面的密码结果系统会去比对
     * @param username 登录时的用户名
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user=userMapper.loadUserByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
//        查询用户角色
        user.setRoles(userMapper.getUserRolesById(user.getId()));
        return user;
    }
}

7、config
①mybatis配置
@Configuration
@MapperScan(basePackages = "com.lc.securitydb.mapper")
public class MybatisConfig {
}

②security配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    /**
     * 进行用户名和密码的配置管理
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }

    /**
     * 加密器
     * @return
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("admin")
                .antMatchers("/dba/**").hasRole("dba")
                .antMatchers("/user/**").hasRole("user")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .permitAll()
                .and()
                .csrf().disable();
    }
}
8、yaml
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/securitytest?useUnicode=true&characterEncoding=utf-8
    username: root
    password: 123456

9、controller
@RestController
public class HelloController {

    /**
     * 登录即可 访问
     * @return
     */
    @GetMapping("/hello")
    public String hello() {
        return "hello security";
    }

    /**
     * 角色dba访问
     * @return
     */
    @GetMapping("/dba/hello")
    public String dba() {
        return "hello dba";
    }

    /**
     * 角色admin访问
     * @return
     */
    @GetMapping("/admin/hello")
    public String admin() {
        return "hello admin";
    }

    /**
     * 角色user访问
     * @return
     */
    @GetMapping("/user/hello")
    public String user() {
        return "hello user";
    }

}

七、SpringSecurity中的继承关系

1、版本化的配置差异
①SpringBoot2.0.8(含)之前的写法

角色之间的继承关系用 空格 分开

@Bean	
RoleHierarchy roleHierarchy() {	
    RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();	
    String hierarchy = "ROLE_dba > ROLE_admin ROLE_admin > ROLE_user";	
    roleHierarchy.setHierarchy(hierarchy);	
    return roleHierarchy;	
}
②SpringBoot2.0.8之后的版本

角色之前的继承用 \n 分开

    @Bean
    public RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        String hierarchy = "ROLE_dba > ROLE_admin \n ROLE_admin > ROLE_user";
        roleHierarchy.setHierarchy(hierarchy);
        return roleHierarchy;
    }

拥有dba角色可以访问admin有关的接口,拥有admin角色可以访问user有关的接口,user只能访问自己有关的接口,所以dba可以访问admin和user有关的接口,admin可以访问user的有关接口

2、注意的几个地方:

①注意角色前要加 ROLE_ 前缀

②角色和角色之间的定义 > 前后要加空格 , \n 前后也要加空格

3、基本配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    /**
     * 进行用户名和密码的配置管理
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }

    /**
     * 加密器
     *
     * @return
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("admin")
                .antMatchers("/dba/**").hasRole("dba")
                .antMatchers("/user/**").hasRole("user")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .permitAll()
                .and()
                .csrf().disable();
    }

    @Bean
    public RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        String hierarchy = "ROLE_dba > ROLE_admin \n ROLE_admin > ROLE_user";
        roleHierarchy.setHierarchy(hierarchy);
        return roleHierarchy;
    }
}

八、动态配置权限

1、sql

菜单表,菜单角色表,用户表,角色表,用户角色表

CREATE TABLE `menu` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `pattern` varchar(100) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of menu
-- ----------------------------
INSERT INTO `menu` VALUES ('1', '/dba/**');
INSERT INTO `menu` VALUES ('2', '/admin/**');
INSERT INTO `menu` VALUES ('3', '/user/**');

CREATE TABLE `menu_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `mid` int(11) DEFAULT NULL,
  `rid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of menu_role
-- ----------------------------
INSERT INTO `menu_role` VALUES ('1', '1', '1');
INSERT INTO `menu_role` VALUES ('2', '2', '2');
INSERT INTO `menu_role` VALUES ('3', '3', '3');

CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) DEFAULT NULL,
  `nameZh` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES ('1', 'dba', '数据库管理员');
INSERT INTO `role` VALUES ('2', 'admin', '系统管理员');
INSERT INTO `role` VALUES ('3', 'user', '用户');

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `enabled` tinyint(1) DEFAULT NULL,
  `locked` tinyint(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user 密码123
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'root', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
INSERT INTO `user` VALUES ('2', 'admin', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
INSERT INTO `user` VALUES ('3', 'sang', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');

-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `uid` int(11) DEFAULT NULL,
  `rid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES ('1', '1', '1');
INSERT INTO `user_role` VALUES ('2', '1', '2');
INSERT INTO `user_role` VALUES ('3', '2', '2');
INSERT INTO `user_role` VALUES ('4', '3', '3');
SET FOREIGN_KEY_CHECKS=1;
2、pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.5.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>org.lc</groupId>
	<artifactId>security-dynamic</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>security-dynamic</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.1.2</version>
		</dependency>

		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid-spring-boot-starter</artifactId>
			<version>1.1.18</version>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
			<version>5.1.27</version>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<resources>
			<resource>
				<directory>src/main/java</directory>
				<includes>
					<include>**/*.xml</include>
				</includes>
			</resource>
			<resource>
				<directory>src/main/resources</directory>
			</resource>
		</resources>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

3、yaml配置
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/securitydy?useUnicode=true&characterEncoding=UTF-8
    username: root
    password: 123456
4、实体配置

menu==>

@Getter
@Setter
@ToString
public class Menu {
    private Integer id;

    /**
     * 访问此菜单的路径
     */
    private String pattern;

    /**
     * 访问此此单需要的角色
     */
    private List<Role> roles;
}

role==>

@Getter
@Setter
@ToString
public class Role {
    private Integer id;
    private String name;
    private String nameZh;
}

user==>

public class User implements UserDetails {

    private Integer id;
    private String username;
    private String password;
    private Boolean enabled;
    private Boolean locked;
    private List<Role> roles;

    public List<Role> getRoles() {
        return roles;
    }

    public User setRoles(List<Role> roles) {
        this.roles = roles;
        return this;
    }

    public Integer getId() {
        return id;
    }

    public User setId(Integer id) {
        this.id = id;
        return this;
    }

    public User setUsername(String username) {
        this.username = username;
        return this;
    }

    public User setPassword(String password) {
        this.password = password;
        return this;
    }

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

    public User setLocked(Boolean locked) {
        this.locked = locked;
        return this;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities=new ArrayList<>();
        for (Role role : roles) {
//           此时这里我们不需要加ROLE_前缀 因为数据库中我们的角色名已经加好了前缀
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

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

    @Override
    public boolean isAccountNonLocked() {
        return !locked;
    }

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

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

5、mapper
public interface MenuMapper {
    List<Menu> getAllMenus();
}

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="org.lc.securitydynamic.mapper.MenuMapper">

    <resultMap id="BaseResultMap" type="org.lc.securitydynamic.bean.Menu">
        <id property="id" column="id"/>
        <result property="pattern" column="pattern"/>
        <collection property="roles" ofType="org.lc.securitydynamic.bean.Role">
            <id property="id" column="rid"/>
            <result property="name" column="rname"/>
            <result property="nameZh" column="rnameZh"/>
        </collection>
    </resultMap>

    <select id="getAllMenus" resultMap="BaseResultMap">
        SELECT
            menu.*, role.id rid,
            role. NAME rname,
            role.nameZh rnameZh
        FROM
            menu
        LEFT JOIN menu_role mr ON menu.id = mr.mid
        LEFT JOIN role ON mr.rid = role.id
    </select>

</mapper>
public interface UserMapper {

    User getUserByUsername(String username);

    List<Role> getRolesByUserId(Integer id);
}

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="org.lc.securitydynamic.mapper.UserMapper">

    <select id="getUserByUsername" resultType="org.lc.securitydynamic.bean.User">
        select * from user where username=#{username}
    </select>

    <select id="getRolesByUserId" resultType="org.lc.securitydynamic.bean.Role">
        select * from role where id in(select rid from user_role where uid=#{id})
    </select>
</mapper>
6、service
@Service
public class MenuService {

    @Autowired
    private MenuMapper menuMapper;

    public List<Menu> getAllMenus() {
        return menuMapper.getAllMenus();
    }
}

@Service
public class UserService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        User user= userMapper.getUserByUsername(s);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        user.setRoles(userMapper.getRolesByUserId(user.getId()));
        return user;
    }
}
7、controller
@RestController
public class HelloController {

    /**
     * 登录即可访问
     * @return
     */
    @GetMapping("/hello")
    public String hello() {
        return "hello security";
    }

    /**
     * 模拟 dba角色
     * @return
     */
    @GetMapping("/dba/hello")
    public String dba() {
        return "hello dba";
    }

    /**
     * 模拟admin角色
     * @return
     */
    @GetMapping("/admin/hello")
    public String admin() {
        return "hello admin";
    }

    /**
     * 模拟user角色
     * @return
     */
    @GetMapping("/user/hello")
    public String user() {
        return "hello user";
    }

}

8、核心配置
①mybati配置
@Configuration
@MapperScan(basePackages = "org.lc.securitydynamic.mapper")
public class MybatisConfig {
}
②过滤回调配置(筛选请求需要的角色)
@Component
public class MyFilterInvocation implements FilterInvocationSecurityMetadataSource {

    /**
     * 匹配ant风格的路径 使用AntPathMatcher类比较两个路径是否匹配
     */
    AntPathMatcher antPathMatcher=new AntPathMatcher();

    @Autowired
    private MenuService menuService;

    /**
     * 这里每一次请求就会查询一遍表 和遍历一遍集合,我们可以放入redis缓存中
     *
     * 根据请求的地址 分析 出需要的角色
     * @param o 实际上是一个FilterInvocation对象 请求的相关信息
     * @return 返回需要的角色
     * @throws IllegalArgumentException
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
//        获取当前请求的url
        String requestUrl = ((FilterInvocation) o).getRequestUrl();
//        获取数据库中每个菜单请求对应的访问路径即角色信息
        List<Menu> allMenus = menuService.getAllMenus();
//        遍历数据库中的菜单
        for (Menu menu : allMenus) {
//            判断数据库中的菜单路径 是否 和请求的路径匹配
            if (antPathMatcher.match(menu.getPattern(), requestUrl)) {
//                若匹配 获取该路径下的对应的所有角色信息
                List<Role> roles = menu.getRoles();
//                定义存放角色的字符串 并指定和list中相同的长度
                String[] allRoleList=new String[roles.size()];
//                遍历该角色集合
                for (int i = 0; i < roles.size(); i++) {
//                    存放到字符串数组中
                    allRoleList[i] = roles.get(i).getName();
                }
//                返回该匹配的请求路径下的所有角色信息
                return SecurityConfig.createList(allRoleList);
            }
        }
//       如果请求的路径没有和数据库中的路径匹配 则返回自定义的角色(这里,我们只作为一个标识,后期根据标识再去处理)
        return SecurityConfig.createList("ROLE_login");


    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    /**
     * 是否支持这种方式 返回true即可
     * @param aClass
     * @return
     */
    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

③存储决定管理器(比对登录用户的角色)
package org.lc.securitydynamic.config;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import java.util.Collection;

/**
 * @BelongsProject: security-dynamic
 * @BelongsPackage: org.lc.securitydynamic.config
 * @Author: lc
 * @CreateTime: 2020-03-29 00:19
 * @Description:
 */
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {

    /**
     * @param authentication 当前登录用户的信息
     * @param o 当前的请求对象(相当于FilterInvocation对象)
     * @param collection 我们自定义的FilterInvocationSecurityMetadataSource的返回匹配路径需要的角色对象
     * @throws AccessDeniedException
     * @throws InsufficientAuthenticationException
     */
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {

//        这里考虑 未登录先直接抛出异常***************
        if (authentication instanceof AnonymousAuthenticationToken) {
            throw new AccessDeniedException("未登录,非法请求");
        }

//        遍历该请求路径下需要的角色对象
        for (ConfigAttribute configAttribute : collection) {
//            如果没有任何路径匹配(我们之前定义的没有任何路径能够匹配上,自定义返回的角色)
            if ("ROLE_login".equals(configAttribute.getAttribute())) {
//                若登录状态为匿名用户(未登录) 直接抛异常
                if (authentication instanceof AnonymousAuthenticationToken) {
                    //RememberMeAuthenticationToken (org.springframework.security.authentication)
                    //TestingAuthenticationToken (org.springframework.security.authentication)
//                    匿名用户的登录
                    //AnonymousAuthenticationToken (org.springframework.security.authentication)
                    //RunAsUserToken (org.springframework.security.access.intercept)
//                    已经进行用户名和密码的登录
                    //UsernamePasswordAuthenticationToken (org.springframework.security.authentication)
                    //PreAuthenticatedAuthenticationToken (org.springframework.security.web.authentication.preauth)

                    throw new AccessDeniedException("非法请求");
                }else{
//                    已经登录 但是没有匹配数据库中的路径
//                    可能只要登录即可访问
//                    直接退出角色寻找
                    return;
                }
            }
//            当前登录用户具备的角色
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
//            遍历用户具备的角色
            for (GrantedAuthority authority : authorities) {
//                如果当前登录用户的角色和 该路径下的数据库的角色匹配
                if (authority.getAuthority().equals(configAttribute.getAttribute())) {
//                    直接返回
                    return;
                }
            }
        }
//        若路径匹配但是 没有找到相应的角色 抛出异常
        throw new AccessDeniedException("权限不足,非法请求");
    }

    /**
     * 是否支持这种方式  true
     * @param configAttribute
     * @return
     */
    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    /**
     * 是否支持这种方式 true
     * @param aClass
     * @return
     */
    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}


④security配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    @Autowired
    private MyFilterInvocation myFilterInvocation;

    @Autowired
    private MyAccessDecisionManager myAccessDecisionManager;


    @Bean
    BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
//                定义请求之前的处理器
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
//                       设置自定义拦截请求需要的角色
                        o.setSecurityMetadataSource(myFilterInvocation);
//                        设置自定义角色比对管理器
                        o.setAccessDecisionManager(myAccessDecisionManager);
                        return o;
                    }
                })
                .and()
                .formLogin()
                .permitAll()
                .and()
                .csrf().disable();
    }
}
9 、执行流程分析

首先拦截所有请求通过过滤回调配置文件处理(MyFilterInvocation),获得当前请求的路径(String requestUrl = ((FilterInvocation) o).getRequestUrl();)

查找所有的请求路径所需的角色信息,遍历该路径信息,如果找到与请求向匹配的路径,则返回该路径所需要的全部角色信息。若没有匹配上,则返回一个标识角色代表没有匹配上(return SecurityConfig.createList("ROLE_login"))

来到角色比对管理器(MyAccessDecisionManager),首先我们可以直接判断用户是否登录,若未登录,直接抛出非法请求异常(throw new AccessDeniedException("未登录,请求非法"))。若进入登录状态验证后,遍历返回的该路径下需要的角色信息,若匹配到我们自定义的标识角色,则代表未匹配路径,则直接返回(代表该路径不需要角色,登录即可访问)。否则代表匹配上指定的路径,那么遍历当前用户登录的角色信息,查看数据库路径下的角色信息是否和当前用户登录所有的角色信息匹配,若匹配到,直接返回。遍历完之后,还没有符合的条件,直接抛出异常。