Spring Security 安全框架

一、Spring Security 安全框架

1.介绍

Spring Security框架为我们提供了用户认证和授权的能力。

  • 用户认证: 登陆验证
  • 用户授权:某个用户是否具有某个权限。具有相应权限的用户才能进行具体的操作。比如: 管理员能进行工资设置,但是普通用户只能查看工资。

2.初体验

1) 引入依赖

 		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

2)设计接口

在访问接口时就会自动的被security的登陆页面拦截

image-20210316141710306

用户名: user

密码: 控制台会提示的

image-20210316141749641

3.过滤器链

security实际上就是一串过滤器链。通过过滤器链来实现用户验证和授权。

image-20210316142759554

4.多种方式设置用户名和密码

方式一:yml配置文件

# 设置用户名和密码
spring:
  security:
    user:
      name: qfadmin
      password: 123456

方式二: 设置配置类的方式

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

        String password = passwordEncoder.encode("123456");

        auth.inMemoryAuthentication().withUser("xiaoming").password(password).roles();

    }

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

方式三:设置自定义登陆页面

步骤:

  • 创建了一个UserDetailsService接口的实现类
/**
 * 创建一个UserDetailsService实现类来设置用户名和密码
 */
@Service("userDetailsService")
public class MyUserService implements UserDetailsService {
    
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        //设置角色,角色的概念在之后章节介绍
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
        //模拟从数据库获取用户名和密码
        User qfadmin = new User("qfadmin", passwordEncoder.encode("123456"), auths);

        return qfadmin;
    }
}

  • 编写配置类
@Configuration
public class SecurityConfigPro extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/login.html") //设置自定义登陆页面
                .loginProcessingUrl("/usr/login") //登陆时访问的路径
                .defaultSuccessUrl("/index").permitAll() //登陆成功后跳转的路径
                .and().authorizeRequests()
                .antMatchers("/","/hello","/user/login").permitAll() //设置可以直接访问的路径,取消拦截
                .anyRequest().authenticated()
                .and().csrf().disable(); //关闭csrf防护
    }
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

  • 设计自定义登陆页面

注意: 表单中的用户名和密码的name属性必须是: username、password

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
    <title>Bootstrap 101 Template</title>

    <!-- Bootstrap -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" rel="stylesheet">

    <!-- HTML5 shim 和 Respond.js 是为了让 IE8 支持 HTML5 元素和媒体查询(media queries)功能 -->
    <!-- 警告:通过 file:// 协议(就是直接将 html 页面拖拽到浏览器中)访问页面时 Respond.js 不起作用 -->
    <!--[if lt IE 9]>
    <script src="https://cdn.jsdelivr.net/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/respond.js@1.4.2/dest/respond.min.js"></script>
    <![endif]-->
</head>
<body>

<form class="form-horizontal" action="/usr/login" method="post">
    <div class="form-group">
        <label for="inputEmail3" class="col-sm-2 control-label">Email</label>
        <div class="col-sm-10">
            <input type="text" name="username" class="form-control" id="inputEmail3" placeholder="Email">
        </div>
    </div>
    <div class="form-group">
        <label for="inputPassword3" class="col-sm-2 control-label">Password</label>
        <div class="col-sm-10">
            <input type="password" name="password" class="form-control" id="inputPassword3" placeholder="Password">
        </div>
    </div>
    <div class="form-group">
        <div class="col-sm-offset-2 col-sm-10">
            <div class="checkbox">
                <label>
                    <input type="checkbox"> Remember me
                </label>
            </div>
        </div>
    </div>
    <div class="form-group">
        <div class="col-sm-offset-2 col-sm-10">
            <button type="submit" class="btn btn-default">Sign in</button>
        </div>
    </div>
</form>

<!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
<script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"></script>
<!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
</body>
</html>

5.角色和权限的四个方法

1)角色和权限的概念

某个角色,拥有怎样的权限。

比如说管理员角色,拥有文件管理、日志管理的权限

比如说 普通用户角色,拥有文件查看的权限。

对于角色和权限来说,必须得看用户属于哪个角色,于是具有相应的权限。

比如小明属于普通用户,那么小明只有查看文件的权限。

image-20210316154406069

2)hasAuthority、hasAnyAuthority、hasRole、hasAnyRole

判断当前登陆成功的用户,能否具有访问指定路径的权限的四个方法:

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/login.html") //设置自定义登陆页面
                .loginProcessingUrl("/usr/login") //登陆时访问的路径
                .defaultSuccessUrl("/index").permitAll() //登陆成功后跳转的路径
                .and().authorizeRequests()
                .antMatchers("/","/hello","/usr/login").permitAll() //设置可以直接访问的路径,取消拦截
            
            
                //1.hasAuthority方法:当前登陆用户,只有具有admin权限才可以访问这个路径
//                .antMatchers("/index").hasAuthority("admins")
                //2.hasAnyAuthority方法:当前登陆用户,具有指定的多个权限中的某一个权限即可访问
                //.antMatchers("/index").hasAnyAuthority("admins","admin")
                //3.hasRole:当前登陆用户是否属于指定的角色
//                .antMatchers("/index").hasRole("student")
                //4.hasAnyRole: 当前登陆用户满足某一个角色即可
                .antMatchers("/index").hasAnyRole("student","admin")
                .anyRequest().authenticated()
                .and().csrf().disable(); //关闭csrf防护
    }

还需要修改,UserDetailsService中的auths集合,例子中直接写死了权限和角色,实际可以从数据库获取并返回。

 @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        //设置角色,角色的概念在之后章节介绍
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role,admin,ROLE_student");
        //模拟从数据库获取用户名和密码,且当前用户有admin的权限
        User qfadmin = new User("qfadmin", passwordEncoder.encode("123456"), auths);

        return qfadmin;
    }

6.security中的常用注解

  • Secured: 具有指定的角色权限才能访问注解的方法
 	@RequestMapping("/items")
    @Secured({"ROLE_student1"})
    @ResponseBody
    public String items(){
        return "items";
    }

  • PreAuthorize: 在访问方法之前先进行角色和权限的验证
 @RequestMapping("/items")
    @PreAuthorize("hasAnyAuthority('admin')")
    @ResponseBody
    public String items(){
        return "items";
    }

  • PostAuthorize: 先执行方法,再进行角色和权限的验证
@RequestMapping("/postItems")
    @PostAuthorize("hasAnyAuthority('teacher')")
    @ResponseBody
    public String postItems(){
        //先执行方法内容,再做权限校验
        System.out.println("show detail here...");
        return "show post items";
    }


  • PostFilter:返回到前端的数据会被过滤,过滤条件在注解中声明
    @RequestMapping("/users")
    @PreAuthorize("hasAnyAuthority('admin')")
    @PostFilter("filterObject.name=='xiaoming'")
    @ResponseBody
    public List<User> users(){

        List<User> users = new ArrayList<>();
        users.add(new User(1001L,"xiaoming"));
        users.add(new User(1002L,"xiaowang"));


        return users;
    }

  • PreFilter:只有符合条件的数据才会被传入方法,条件在注解中声明
	@RequestMapping("/preFilterItems")
    @PreAuthorize("hasAnyAuthority('admin')")
    @PreFilter(value="filterObject.userName == 'xiaoming'")
    public List<User> getUsersByPreFilter(@RequestBody List<User> list){
        //只有userName是'xiaoming'的数据才会被传入
        list.forEach(t->{
            System.out.println(t.getUserName());
        });
        return list;
    }

关于如何校验数据库保存的密码。

 	@Test
    public void testPassword(){
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        //1.数据库中要存密码的密文

        String password = "abc123";//明文
        String encodePassword = passwordEncoder.encode(password);


        //2.用户输入一个明文,怎么和数据库的密文进行比较
        boolean matches = passwordEncoder.matches(password, encodePassword);
        System.out.println(matches);

    }

二、实现注销功能

1.注销的流程

image-20210317091908382

2.步骤

1)在配置类中添加注销配置

@Override
    protected void configure(HttpSecurity http) throws Exception {
        //配置没有权限的跳转页面
        http.exceptionHandling().accessDeniedPage("/error.html");

        //注销的配置
        http.logout().logoutUrl("/logout") //注销时访问的路径
                .logoutSuccessUrl("/logoutSuccess").permitAll(); //注销成功后访问的路径

        http.formLogin()
                .loginPage("/login.html") //设置自定义登陆页面
                .loginProcessingUrl("/usr/login") //登陆时访问的路径
                .defaultSuccessUrl("/index").permitAll() //登陆成功后跳转的路径
                .and().authorizeRequests()
                .antMatchers("/","/hello","/user/login").permitAll() //设置可以直接访问的路径,取消拦截
                //1.hasAuthority方法:当前登陆用户,只有具有admin权限才可以访问这个路径
//                .antMatchers("/index").hasAuthority("admins")
                //2.hasAnyAuthority方法:当前登陆用户,具有指定的多个权限中的某一个权限即可访问
                //.antMatchers("/index").hasAnyAuthority("admins","admin")
                //3.hasRole:当前登陆用户是否属于指定的角色
//                .antMatchers("/index").hasRole("student")
                //4.hasAnyRole: 当前登陆用户满足某一个角色即可
//                .antMatchers("/index").hasAnyRole("student","admin")
                .anyRequest().authenticated()
                .and().csrf().disable(); //关闭csrf防护
    }

2)创建注销链接,链接中访问注销路径/logout

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"> 
    <title>首页</title>
</head>
<body>
  欢迎来到主页!<a href="/logout">注销</a>
</body>
</html>

3)编写注销成功后的跳转接口

    @RequestMapping("/logoutSuccess")
    public String logoutSuccess(){
        return "logoutsuccess";
    }

posted @ 2021-07-21 14:37  牛奶配苦瓜  阅读(443)  评论(0编辑  收藏  举报