Spring Security学习

1.初步搭建集成了Spring Security的工程

1.1 新建Maven项目

<!--简单的集成了spring security的web工程只需要如下两个依赖-->
<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>
</dependencies>

1.2 新建Controller层接口

// 新建一个简单的接口,便于观察spring security的登录拦截
@RestController
@RequestMapping("/hello")
public class Hello {
    @GetMapping("/world")
    public String world() {
        return "world";
    }
}

1.3 浏览器访问该接口

浏览器输入:http://localhost:8080/hello/world

此时spring security监测到用户尚未登录,会出现一个登录界面,该界面的用户为user,密码为项目控制台输出的如:Using generated security password: fe4ce183-45db-4d7b-8e74-af9a0109ce85,这是一个临时的密码,每次运行都会变化,因此读者要以自己的控制台输出的为主。输入用户名和密码后,就可以看到网页返回字符串world了。
默认登录页
通过上述步骤,可以初步了解spring security框架。

2. 进一步学习Spring Security的基本能力

2.1 关闭安全管理框架

如果需要关闭安全管理框架,可以在项目启动类上增加如下代码,表示:排除Security的配置,让其不生效

// 排除Security的配置,让其不生效
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)

排除Security的配置后,就可以直接访问hello/world接口了。

2.2 配置文件的方式自定义登录用户名和密码

通过在application.properties配置文件中增加以下内容,创建一个用户,并且可以使用该用户登录。**注意:**一旦创建了用户,控制台就不会再打印user用户的密码了。

# 配置文件的方式自定义security的登录用户和密码
spring.security.user.name=kevin
spring.security.user.password=123456

按照1.3节的步骤,使用kevin123456进行登录,可以正常登录。

2.3 内存中配置登录用户

使用内存中配置的用户,需要创建一个配置文件,并且继承:WebSecurityConfigurerAdapter类,实现其:configure(AuthenticationManagerBuilder auth)方法。

@Configuration //表示该类为配置类,等同于一个xml文件
@EnableWebSecurity //表示使用Spring Security的安全功能
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
 		// 在内存中构建一个用户名:wang,密码:123的用户
        PasswordEncoder pe = passwordEncoder();
        auth.inMemoryAuthentication().withUser("wang").password(pe.encode("123")).roles();
    }
    
    // 注入一个密码加密器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

按照1.3节的步骤,使用wang123进行登录,可以正常登录。

说明: 本文使用的Spring Security的版本是5.1.1,该版本中,不允许使用明文密码,因此需要注入一个密码加密器,并对密码进行加密。

2.4 基于角色的身份认证

想要做到基于角色的身份认证,首先需要:启用方法级别的认证。在步骤2.3中创建的SecurityConfig类上增加注解:@EnableGlobalMethodSecurity(prePostEnabled = true),同时创建三个用户,代码如下:

// 启用方法级别的认证,prePostEnabled默认为false,为true时,可以使用@PreAuthorize(指定在方法访问之前进行角色的认证)和@PostAuthorize
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        PasswordEncoder pe = passwordEncoder();
        auth.inMemoryAuthentication().withUser("wang").password(pe.encode("123")).roles();

        auth.inMemoryAuthentication().withUser("admin").password(pe.encode("admin")).roles("admin");

        auth.inMemoryAuthentication().withUser("zhangsan").password(pe.encode("123")).roles("normal");

        auth.inMemoryAuthentication().withUser("kevin").password(pe.encode("123")).roles("normal", "admin");
    }

    // 注入一个密码加密器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Controller中,创建需要不同角色才能访问的接口,内容如下:

// 只有admin权限的角色才能访问的接口
@PreAuthorize(value = "hasAnyRole('admin')")
@GetMapping("/admin")
public String admin() {
    return "admin role";
}

// 只有normal权限的角色才能访问的接口
@PreAuthorize(value = "hasAnyRole('normal')")
@GetMapping("/normal")
public String normal() {
    return "normal role";
}

// admin和normal权限的角色都能访问的接口
@PreAuthorize(value = "hasAnyRole('admin','normal')")
@GetMapping("/adminAndNormal")
public String adminAndNormal() {
    return "admin and normal role";
}

完成上述的代码后,在浏览器分别输入不同的地址,访问不同的接口,然后,根据访问接口的角色,输入不同的账号和密码,输入正确的情况下,可以正常访问对应的接口,输入错误的话,会有相应的提示,如果是权限不足,则会出现403界面。
403界面
至此,完成了Spring Security基本的学习,接下来会进入更加实际的使用中,会另开一个项目。

3. 结合数据库进行用户的操作

从数据库中获取用户的身份信息(用户名称,密码,角色)。在Spring Security框架中对于用户信息的表示类是UserDetailsUserDetails是一个接口,是高度抽象的用户信息类(相当于项目中的User类)。

org.springframework.security.core.userdetails.User类是UserDetails接口的实现类,构造方法有三个参数:User(String username, String password, Collection<? extends GrantedAuthority> authorities)。需要向Spring Security提供User对象,这个对象的数据来自数据库的查询。

实现UserDetailsService接口,重写方法:UserDetails loadUserByUsername(String username),根据用户名执行数据库的查询,得到数据库中的用户信息。

3.1 新建Maven工程

新建一个Maven工程,对其进行配置,使其能够访问数据库。本章节使用的是Mybatis Plus框架来操作数据库的。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-test</artifactId>
        <scope>test</scope>
    </dependency>

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

    <!-- mybatis-plus -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.3.2</version>
    </dependency>
    <!-- mybatis plus 代码生成器依赖 -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-generator</artifactId>
        <version>3.3.2</version>
    </dependency>
    <!-- 代码生成器模板 -->
    <dependency>
        <groupId>org.freemarker</groupId>
        <artifactId>freemarker</artifactId>
    </dependency>

    <!--   mysql配置     -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.20</version>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.20</version>
    </dependency>
</dependencies>

application.properties配置文件信息如下,具体数据库信息,需要自行修改。

# 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/kevin?useUnicode=true&useSSL=false&characterEncoding=utf-8&serverTimezone=GMT
spring.datasource.username=root
spring.datasource.password=99.99

# 连接池配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

# 驱动配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

数据表信息:

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `role` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

有了数据表,需要对其进行数据的填充,数据库中的密码一栏内容需要是加密的,这里的加密方式需要与下面3.3节Spring Security配置类的加密方式一致。具体的加密方式以及密码,都需要根据实际的需求来确定。以下仅为一种方式:

// 生成密码
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
System.out.println(bCryptPasswordEncoder.encode("123456"));

Mybatis plus代码生成类内容如下,具体的数据库信息,需要按照实际修改:

public class CodeGenerator {
    /**
     * 作者
     */
    private static final String AUTHOR = "kevin";

    /**
     * 表名 多个表名之间用逗号分隔
     */
    private static final String TABLE_NAME = "user";

    public static void main(String[] args) {
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator();

        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/src/main/java");
        gc.setAuthor(AUTHOR);
        gc.setOpen(false);
        gc.setServiceName("%sService"); // service 命名方式
        gc.setServiceImplName("%sServiceImpl"); // service impl 命名方式

        // 自定义文件命名,注意 %s 会自动填充表实体属性!
        gc.setMapperName("%sMapper");
        gc.setXmlName("%sMapper");
        gc.setFileOverride(true);
        gc.setActiveRecord(true);

        gc.setEnableCache(false); // XML 二级缓存
        gc.setBaseResultMap(true); // XML ResultMap

        gc.setBaseColumnList(false); // XML columnList
        mpg.setGlobalConfig(gc);

        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl(
                "jdbc:mysql://127.0.0.1:3306/kevin?useUnicode=true&useSSL=false&characterEncoding=utf-8&serverTimezone=GMT");
        // dsc.setSchemaName("public");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("99.99");
        mpg.setDataSource(dsc);

        // 包配置
        PackageConfig pc = new PackageConfig();
        pc.setParent("com.example.security_learn1");
        pc.setEntity("entity");
        pc.setService("service");
        pc.setServiceImpl("service.impl");
        mpg.setPackageInfo(pc);

        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };

        // 如果模板引擎是 freemarker
        String templatePath = "/templates/mapper.xml.ftl";

        // 自定义输出配置
        List<FileOutConfig> focList = new ArrayList<>();
        // 自定义配置会被优先输出
        focList.add(new FileOutConfig(templatePath) {
            @Override
            public String outputFile(TableInfo tableInfo) {
                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
                String moduleName = pc.getModuleName() == null ? "" : pc.getModuleName();
                return projectPath + "/src/main/resources/mapper/" + moduleName + "/" + tableInfo.getEntityName()
                        + "Mapper" + StringPool.DOT_XML;
            }
        });

        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);

        // 配置模板
        TemplateConfig templateConfig = new TemplateConfig();

        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);

        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);

        strategy.setEntityLombokModel(true);
        strategy.setRestControllerStyle(true);
        strategy.setInclude(TABLE_NAME);
        strategy.setControllerMappingHyphenStyle(true);
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }
}

修改完数据库信息后,运行CodeGenerator类,生成各个层的相关信息。

3.2 新建UserDetailsService接口的实现类

实现类的内容如下:

@Component("UserDetailsServiceImpl")
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    @Qualifier("MyUserMapper")
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        org.springframework.security.core.userdetails.User returnUser = null;
        if (username != null) {
            User one = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, username));
            if (one != null) {
                // Security中的角色必须以"ROLE_"开头,源代码中可以看到该规则
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + one.getRole());
                returnUser = new org.springframework.security.core.userdetails.User(username, one.getPassword(), Arrays.asList(authority));
            } else {
                throw new UsernameNotFoundException("can not find user by username:" + username);
            }
        } else {
            throw new UsernameNotFoundException("username can not null.");
        }
        return returnUser;
    }
}

3.3 新建Spring Security配置类

配置类的内容如下:

// 启用方法级别的认证,prePostEnabled默认为false,为true时,可以使用@PreAuthorize(指定在方法访问之前进行角色的认证)和@PostAuthorize
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    @Qualifier("UserDetailsServiceImpl")
    private UserDetailsService userDetailsService;

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

3.4 编写Controller中的接口,然后测试

在`Controller`中,创建需要不同角色才能访问的接口,内容如下:

​```java
// 只有admin权限的角色才能访问的接口
@PreAuthorize(value = "hasAnyRole('admin')")
@GetMapping("/admin")
public String admin() {
    return "admin role";
}

// 只有normal权限的角色才能访问的接口
@PreAuthorize(value = "hasAnyRole('normal')")
@GetMapping("/normal")
public String normal() {
    return "normal role";
}

// admin和normal权限的角色都能访问的接口
@PreAuthorize(value = "hasAnyRole('admin','normal')")
@GetMapping("/adminAndNormal")
public String adminAndNormal() {
    return "admin and normal role";
}
​```

完成上述的代码后,在浏览器分别输入不同的地址,访问不同的接口,然后,根据访问接口的角色,输入不同的账号和密码,输入正确的情况下,可以正常访问对应的接口,输入错误的话,会有相应的提示,如果是权限不足,则会出现403界面。

4. 基于角色的权限

4.1 认证和授权

  • 认证:authentication,访问者是谁。
  • 授权:authorization,访问者能够做什么。

举例:张三想要查看公司的流水信息。首先,要对张三的身份进行认证,即:判断张三是不是本公司的人员。确定张三是公司的人员后,查看张三的授权信息,即:判断张三有没有权限查看公司的流水信息。

4.2 RBAC是什么?

RBAC是基于角色的访问控制(Role-Based Access Control)

RBAC中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来也很方便。

其基本思想是,对操作系统的各种权限不是直接授予具体的用户,而是在用户集合与权限集合之间建立一个角色集合。每一种角色对应一组响应的权限,一旦用户被分配了适当的角色后,该用户就拥有此角色的所有操作权限。这样做的好处是,不必在每次创建用户时都进行分配权限的操作,只要分配相应的角色即可。而且角色的权限变更相比较用户的权限变更要少得多,这样将简化用户的权限管理,减少系统的开销。

权限:能对资源的操作,比如增加,修改,删除,查看等等。

角色:自定义的,表示权限的集合。一个角色可以有多个权限。

RBAC设计中的表

  1. 用户表:用户认证(登录使用的表)

    用户名,密码,是否启用等信息

  2. 角色表:定义角色信息

    角色名称,角色的描述等

  3. 用户与角色的关系表

    一个用户可以有多个角色,一个角色可以对应多个用户。两者是多对多的关系。

  4. 权限表

    权限名,权限描述等

  5. 角色与权限的关系表

    一个角色可以有多个权限,一个权限可以对应多个角色。两者是多对多的关系。

4.3 Spring Security中认证的接口和类

4.3.1 UserDetails接口

表示用户信息的接口,主要的方法如下:

/**
只有全部返回true的时候,认证才会通过,在实现类User的7个参数的构造方法中可以看到,对于这些boolean类型的返回值,都是传的true
*/
boolean isAccountNonExpired();	// 账号是否过期
boolean isAccountNonLocked();	// 账号是否锁定
boolean isCredentialsNonExpired();	// 账号证书是否过期 
boolean isEnabled();	// 账号是否启用
Collection<? extends GrantedAuthority> getAuthorities();	// 权限集合

UserDetails接口的实现类:

org.springframework.security.core.userdetails.User

User类是UserDetails接口的一个重要实现类,可以根据系统需要,自定义类实现UserDetails接口,这个类可以交给Spring Security进行认证使用。

4.3.2 UserDetailsService接口

获取用户的信息,得到的是UserDetails接口对象,一般项目中都需要自定义实现类实现这个接口,从数据库中获取数据。只有一个待实现的方法:

UserDetails loadUserByUsername(String username)	// 根据用户的名称,获取用户的信息(用户名称,密码,角色集合,是否可用,是否锁定等信息)

UserDetailsService接口的实现类:

  1. InMemoryUserDetailsManager:在内存中维护用户信息

    优点:使用方便。缺点:数据不是持久的。重启后恢复原样。

  2. JdbcUserDetailsManager:用户信息存放在数据库中,底层使用jdbcTemplate操作数据库。可以用其提供的方法完成用户的管理。数据库文件(users.ddl)在:org.springframework.security.core.userdetails.jdbc

代码地址:代码下载
对于代码的说明均在不同module的HELP.md中。

4.3.3 Spring Security默认的登录页

默认的登录页是form表单,其格式如下:

  1. 访问地址 /login

  2. 请求方式 post

  3. 请求参数

    在configure(HttpSecurity http)里修改

    • 用户名:username,可以自定义,在配置文件中使用.usernameParameter("")修改
    • 密码:password,可以自定义,在配置文件中使用.passwordParameter("")修改

UsernamePasswordAuthenticationFilter过滤器,是对账号和密码的过滤器,默认的用户名和密码的命名就是在这里体现的。

4.3.4 前后端分离的方式,自定义登录页

默认的登录页是表单,仿照其重写后的默认的登录页依旧是表单,对于现在的前后端分离的方式不适合。如果要使用前后端分离,一般使用json作为数据的交换格式,需要使用另一种方式才可以。

ajax方式,用户端发起请求,Srping Security接收请求验证用户的用户名和密码,把验证的结果(json数据)返回给请求方

使用ajax的方式进行登录,有两个处理器需要重写,如下:

AuthenticationSuccessHandler
当框架验证用户信息成功后执行的接口,执行的是如下方法
onAuthenticationSuccess()
AuthenticationFailureHandler
当框架验证用户信息失败后执行的接口,执行的是如下方法
onAuthenticationFailure()
4.3.5 登录页增加验证码

验证的验证使用的是过滤器,整个Spring Security框架都是基于过滤器实现的。

请求的步骤:用户发起的请求 —> 过滤器1 —>过滤器2 —> 要访问的资源

在账号和密码的验证之前,先对验证验证进行验证,这样更加符合实际的设计。

UsernamePasswordAuthenticationFilter过滤器之前,新建一个过滤器,用于验证码的验证

新建过滤器的方式共有两种:

  1. 自定义过滤器
  2. 继承`OncePerRequestFilter

授权表达式举例说明

表达式说明
permitAll永远返回 true
denyAll永远返回 false
anonyous当前用户若是匿名用户返回 true
rememberMe当前用户若是 rememberMe 用户返回 true
authenticated当前用户若不是匿名(已认证)用户返回 true
fullAuthenticated当前用户若既不是匿名用户又不是 rememberMe 用户时返回 true
hasRole(role)当前用户权限集合中若拥有指定的 role 角色权限(匹配时会在你所指定的权限前加’ROLE_’,即判断是否有“ROLE_role”权限)时返回 true
hasAnyRole(role1, role2, ...)当前用户权限集合中若拥有任意一个角色权限时返回 true
hasAuthority(authority)当前用户权限集合中若具有 authority 权限(匹配是否有“authority”权限)时返回 true
hasAnyAuthority(authority)当前用户权限集合中若拥有任意一个权限时返回 true
hasIpAddress("192.168.1.0/24")发送请求的IP匹配时返回 true
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值