Spring Security框架

moran
2021-02-16 / 0 评论 / 3 阅读 / 正在检测是否收录...
温馨提示:
本文最后更新于2021年04月14日,已超过59天没有更新,若内容或图片失效,请留言反馈。

Spring Security



同款产品(Shiro)比较

  • Spring Security
  • Shiro

简单例子

  • 创建SpringBoot项目
    使用Spring Initializr快速创建SpringBoot项目
  • 添加依赖
    勾选Spring-web


勾选Spring-Security

  • 编写测试类
    package com.atguigu.securitydemo1.controller;

    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    @RestController
    @RequestMapping("/test")
    public class TestController {

        @GetMapping("hello")
        public String hello(){
            return "hello security";
        }
    }

通过上述的代码,访问http://localhost:8111/test/hello会出现如下:

这说明spring-security已经整合进来了。
默认的用户名是user,密码输出在控制台。登录成功后就可以进入控制器方法了。

SpringSecurity基本原理

SpringSecurity本质是一个过滤器链。
举三个:

两个重要接口

UserDetailsService接口

当什么东西都不配置时,启动后的用户密码是默认生成的,而实际中是需要重数据库查询出来的。所以需要自定义逻辑来完成。
通过实现UserDetailsService接口可以实现。

PasswordEncoder

数据加密接口。

修改登录的用户名和密码

方式一:通过配置文件

application.properties

server.port=8111

spring.security.user.name=atguigu
spring.security.user.password=atguigu

方式二:通过配置类

另写一个配置类

package com.atguigu.securitydemo1.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String password = passwordEncoder.encode("123");
        auth.inMemoryAuthentication().withUser("lucy").password(password).roles("admin");
    }

    @Bean
    public PasswordEncoder password(){
        return new BCryptPasswordEncoder();
    }
}

注入的bean是因为用到了加密的类BCryptPasswordEncoder,而这个类依赖于PasswordEncoder,因此需要主要他的实现类。

方式三:自定义编写实现类

上述的两种方法都是直接定死了账号密码,而实际中是需要查询数据库的,因此推荐使用第三种。

  • 第一步:创建配置类,指定使用哪个UserDetailsService实现类
    package com.atguigu.securitydemo1.config;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;

    @Configuration
    public class SecurityConfigTest extends WebSecurityConfigurerAdapter {

        @Autowired
        private UserDetailsService userDetailsService;

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            // 指定使用的UserDetailsService实现类和加密的实例对象
            auth.userDetailsService(userDetailsService).passwordEncoder(password());
        }

        @Bean
        public PasswordEncoder password(){
            return new BCryptPasswordEncoder();
        }
    }
  • 第二步:编写实现类,返回User对象,User对象指定用户名和密码还有操作权限
    package com.atguigu.securitydemo1.service;

    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.AuthorityUtils;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.stereotype.Service;

    import java.util.List;

    @Service("userDetailsService")
    public class MyUserDetailsService implements UserDetailsService {
        @Override
        public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
            List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
            return new User("mary",new BCryptPasswordEncoder().encode("123"),auths);
        }
    }

User类是SpringSecurity提供的,是UserDetails的实现类,该类的有参构造接受3个参数(用户名,密码,权限集合)。
注意:注解@Service的value必须和第一步注入的名称一致。

整合数据库实现登录-简单案例

使用MybatisPlus完成数据库操作。

  • 添加依赖
    MybatisPlus和lombok
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.2</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.18</version>
</dependency>
  • 数据库表
  • 数据表对应实体类
    package com.atguigu.securitydemo1.entity;

    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Users {

        private Integer id;
        private String username;
        private String password;
    }
  • 使用MybatisPlus创建dao接口
    package com.atguigu.securitydemo1.Mapper;

    import com.atguigu.securitydemo1.entity.Users;
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;

    public interface UsersMapper extends BaseMapper<Users> {
    }
  • 在MyUserDetailsService中调用mapper方法进行认证
    package com.atguigu.securitydemo1.service;

    import com.atguigu.securitydemo1.Mapper.UsersMapper;
    import com.atguigu.securitydemo1.entity.Users;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.AuthorityUtils;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.stereotype.Service;
    import java.util.List;

    @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);
        }
    }
  • 添加注解MapperScan
    为启动类添加@MapperScan注解,将指定的包下的mapper接口扫描注入到容器中。
  • 配置数据库
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/user?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456

通过上述代码就可以实现数据库账号密码登陆。

自定义设置登录页面

  • 配置类SecurityConfigTest
    package com.atguigu.securitydemo1.config;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;

    @Configuration
    public class SecurityConfigTest extends WebSecurityConfigurerAdapter {

        @Autowired
        private UserDetailsService userDetailsService;

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

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.formLogin()    // 对登录进行配置
                 .loginPage("/login.html")   // 未登录跳转
            .loginProcessingUrl("/user/login")  // 登录提交的接口(Spring Security自动实现)
                .defaultSuccessUrl("/test/index").permitAll()    // 登录成功跳转路径
                .and().authorizeRequests()  // 对登录用户的权限进行配置
                .antMatchers("/","/test/hello","/user/login").permitAll() // 设置哪些路径可以直接访问
                .anyRequest().authenticated()
                    .and().csrf().disable(); // 关闭csrf防护

        }

        @Bean
        public PasswordEncoder password(){
            return new BCryptPasswordEncoder();
        }
    }
  • 创建相关页面
    login.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="/user/login" method="post">
        用户名:<input type="text" name="username">
        <br>
        密码:<input type="text" name="password">
        <br>
        <input type="submit" value="login">
    </form>
</body>
</html>

注意表单中的name和password的name必须是username和password,在SpringSecurity规定了。

  • 控制器方法
    上面配置中成功跳转的接口/test/index
@GetMapping("index")
public String index(){
    return "hello index";
}

通过如上代码配置后,访问/test/hello不需要认证,访问/test/index进入自定义的页面通过数据库账号密码登录。

基于角色和权限进行访问控制

  • hasAuthority()方法
    如果当前的主题具有指定的权限,则返回true,否则返回false。

修改SecurityConfigTest类

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()    // 对登录进行配置
            .loginPage("/login.html")   // 未登录跳转
            .loginProcessingUrl("/user/login")  // 登录提交的接口(Spring Security自动实现)
            .defaultSuccessUrl("/test/index").permitAll()    // 登录成功跳转路径
            .and().authorizeRequests()  // 对登录用户的权限进行配置
            .antMatchers("/","/test/hello","/user/login").permitAll() // 设置哪些路径可以直接访问
                // 当前登录用户,只有具有admins权限才能访问这个路径
                .antMatchers("/test/index").hasAuthority("admins")
            .anyRequest().authenticated()
                .and().csrf().disable(); // 关闭csrf防护

    }

而用户验证的逻辑代码位置在MyUserDetailsService中,为集合添加admins权限

@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,admins");// 添加admins权限
        return new User(users.getUsername(),new BCryptPasswordEncoder().encode(users.getPassword()),auths);
    }

如果用户没有admins权限那么就登录失败(进入403错误页面Forbidden,没有权限)

  • hasAnyAuthority()方法
    如果当前的主体有任何提供的角色的话,返回true,多个权限使用逗号分隔

  • hasRole()方法
    如果用户具备给定角色就允许访问,否则出现403.


注意的是,角色和权限不一样了,不能在权限集合中直接写角色名了,需要使用ROLE_开头,比如角色名是admin,那么集合中就需要写ROLE_admin.

  • hasAnyRole()方法
    表示用户具备任意一个角色都可以访问

具体使用和hasAnyAuthority一致,命名和hasRole一致。

自定义403Forbidden页面

在配置类中进行配置
修改SecurityConfigTest

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling().accessDeniedPage("/unauth.html"); // 无权限跳转的页面    
        http.formLogin()    // 对登录进行配置
            .loginPage("/login.html")   // 未登录跳转
            .loginProcessingUrl("/user/login")  // 登录提交的接口(Spring Security自动实现)
            .defaultSuccessUrl("/test/index").permitAll()    // 登录成功跳转路径
            .and().authorizeRequests()  // 对登录用户的权限进行配置
            .antMatchers("/","/test/hello","/user/login").permitAll() // 设置哪些路径可以直接访问
                // 当前登录用户,只有具有admins权限才能访问这个路径
                .antMatchers("/test/index").hasAuthority("admins")
            .anyRequest().authenticated()
                .and().csrf().disable(); // 关闭csrf防护

    }

注解的使用

使用注解先要开启注解功能
使用@EnableGlobalMethodSecurity(securedEnabled=true)在启动类上标注

@Secured注解

判断是否具有角色,另外需要注意这里匹配的前缀需要加上ROLE_
在控制器方法上使用该注解,标注该方法需要某些角色才能使用

@Secured({"ROLE_sale","ROLE_manager"}) // 拥有任意一个角色就可以访问
@GetMapping("update")
public String update(){
    return "hello update";
}

@PreAuthorize

在方法执行前进行权限认证

@PreAuthorize("hasAnyAuthority('admins')")
@GetMapping("update")
public String update(){
    return "hello update";
}

@PostAuthorize

在方法执行之后进行权限认证
使用方法和@PreAuthorize一致,唯一不同的是该注解会在方法执行完毕后执行权限认证。

PostFileter

对控制器方法返回的数据进行过滤

PreFilter

进入控制器之前对传入的参数进行过滤

用户注销

在配置类中配置
修改SecurityConfigTest配置类

@Override
    protected void configure(HttpSecurity http) throws Exception {
        // 退出
        http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll();
        http.exceptionHandling().accessDeniedPage("/unauth.html");
        http.formLogin()    // 对登录进行配置
            .loginPage("/login.html")   // 未登录跳转
            .loginProcessingUrl("/user/login")  // 登录提交的接口(Spring Security自动实现)
            .defaultSuccessUrl("/success.html").permitAll()    // 登录成功跳转路径
            .and().authorizeRequests()  // 对登录用户的权限进行配置
            .antMatchers("/","/test/hello","/user/login").permitAll() // 设置哪些路径可以直接访问
                // 当前登录用户,只有具有admins权限才能访问这个路径
                .antMatchers("/test/index").hasAuthority("admins")
            .anyRequest().authenticated()
                .and().csrf().disable(); // 关闭csrf防护
    }

登录成功页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    登录成功!
    <a href="/logout">退出</a>
</body>
</html>

执行上面代码后,如果退出后,那么其他需要授权才能访问的页面就需要重新登录才能访问。

实现自动登录(记住我)

原理:

实现:

  • 在配置类中注入数据源和token生成工具
    修改SecurityConfigTest配置类
// 注入数据源
    @Autowired
    private DataSource dataSource;

    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }

注入数据源的原因是因为需要使用SpringSecurity生成token,然后将token存入数据库中。通过jdbcTokenRepository.setCreateTableOnStartup(true);表示启动后创建数据表。

  • 修改配置
    修改SecurityConfigTest配置类
    @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 退出
            http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll();
            http.exceptionHandling().accessDeniedPage("/unauth.html");
            http.formLogin()    // 对登录进行配置
                .loginPage("/login.html")   // 未登录跳转
                .loginProcessingUrl("/user/login")  // 登录提交的接口(Spring Security自动实现)
                .defaultSuccessUrl("/success.html").permitAll()    // 登录成功跳转路径
                .and().authorizeRequests()  // 对登录用户的权限进行配置
                .antMatchers("/","/test/hello","/user/login").permitAll() // 设置哪些路径可以直接访问
                    // 当前登录用户,只有具有admins权限才能访问这个路径
                    .antMatchers("/test/index").hasAuthority("admins")
                .anyRequest().authenticated()
                    .and().rememberMe().tokenRepository(persistentTokenRepository()) // 开启记住我
                    .tokenValiditySeconds(60)   // 设置有效市时长,单位秒
                    .userDetailsService(userDetailsService) 
                    .and().csrf().disable(); // 关闭csrf防护

        }
  • 在页面添加记住我复选框
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="/user/login" method="post">
        用户名:<input type="text" name="username">
        <br>
        密码:<input type="text" name="password">
        <br>
        <input type="checkbox" name="remember-me">自动登录
        <br>
        <input type="submit" value="login">
    </form>
</body>
</html>

CSRF理解


不关闭csrf防护
再通过如下配置,可以开启防护

SpringSecurity微服务

0

评论 (0)

取消