1.简介
当前绝大多数网站都都存在着用户认证
和用户授权
这最基本的功能,关于这两个功能概述如下:
- 用户认证:验证某个用户身份为系统中合法的身份,说白了就是验证用户有没有权限来操作系统某些功能。传统做法通过==用户名==和==密码==来完成认证的功能
- 用户授权:校验某个用户是否有权限去执行某个操作。在一个系统中,不同的用户拥有的权限是不同的。例如:后台管理系统,不同的用户登录进去,看到的界面不同,这就是用户授权
Spring Security 就是一个这样的用户认证与授权框架,其介绍如下:
官网地址:https://spring.io/projects/spring-security
官方文档地址:https://docs.spring.io/spring-security/site/docs/5.4.2/reference/html5/
2.对比
除了Spring Security
可以进行授权认证以外,Apach Shiro
也可以进行授权认证,简单对比如下:
-
使用方面
Shiro
比Spring
更容易使用,实现和最重要的理解,在SSM
阶段,授权认证一致都是Shiro
的天下,虽然Spring Security
已经出现好久,但是由于其配置复杂性,就让很多人望而却步了,同时对于一般的项目来说,Shiro
也完全能够胜任。
但是SpringBoot
以后,它的自动配置功能,简化了Spring Security
配置步骤,只需要使用更少的配置来使用该框架
因此,具体是使用Shiro
还是Security
具体看整个项目的架构,常见组合如下:
ssm + shiro
springboot / spring cloud + spring security
-
其他方面
Spring Security
与Spring
天然无缝结合,同时还提供了 对 OAuth 与 OpenId的支持,但是Shiro
则需要手动实现
3.实现
在这里先通过实现一个最基本的HelloWorld,来了解Spring Security工作原理
-
版本:
Spring Boot : 2.3.7.RELEASE
-
步骤
创建SpringBoot项目,并且导入相关依赖,具体POM文件如下:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- spring security 依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies>
创建
Controller
,具体如下:package com.briup.security.web; 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 add() { return "hello security"; } }
由于SpringBoot项目默认使用8080端口,在这里修改其默认端口,如下:
server.port=8081
启动项目,并且访问地址:http://127.0.0.1:8081/test/hello
发现地址自动跳转到了登录界面,进行授权认证
只要导入
Spring Security
依赖,那么默认就会有一个用户名为user
,其密码为启动时打印出的加密字符串,如下将用户名,密码填充进去即可完成认证,如下
这样就完成了一个基本
Spring Security
的简单用户认证,实际项目中,用户名与密码肯定是要从数据库中查询出来,这里只是做一个简单的认证感受一下该框架的魅力
4.原理
思考:
==从上述的例子中,体验了Spring Security
认证流程,那么它的原理到底是什么呢?==
Spring Security
是 基于 Servlet
过滤器链进行安全认证的,如下:
当客户端发送请求,那么过滤器就会把该请求拦截下来,进行校验,校验通过过滤器则放行,具体的过滤器如下:
从上图中可以看出当用户发送请求,首先经过了用户名密码校验过滤器,我们来看一下该过滤器的源码
UsernamePasswordAuthenticationFilter
部分源码如下:
从源码可以以下特点:
- 先判断认证请求是否是
Post
请求,如果不是则抛出异常 - 再获取用户名密码,进行校验,校验通过则把请求传入下一个过滤器
经过一系列的过滤器最终传入到ExceptionTranslationFilter
,其源码如下:
从图中源码可以看出,在该过滤器中主要是对异常进行处理,如果没有异常,过滤器则直接放行到下一个过滤器FilterSecurityInterceptor
过滤器
FilterSecurityInterceptor
位于过滤器链的最底部,一个方法级别的过滤器,其源码如下:
从源码可以看出在请求放行之前需要先执行之前所有的过滤器,才会进行放行。
总结如下:
5.加载
从上述中,知道Spring Security
本质就是一个过滤器链,通过不同的过滤器组合使用从而实现认证与授权。
==那么这些过滤器是如何被加载的呢,与Spring容器又存在什么关系呢?==
Spring Security
主要是用过DelegatingFilterProxy
去管理过滤器实例。
当然该类也是一个过滤器,使用该类最大的好处就是可以通过Spring
容器来管理 Servler Filter
的生命周期
如果过滤器需要Spring容器中的实例,也可以直接注入
该类部分源码如下:
在该类的源码中,发现在doFilter
方法中会调用initDelegate
方法,该方法源码如下:
该方法的主要作用就是从Spring
容器中拿到代理过滤器实例对象,当该方法执行完毕.
那么doFilter
方法紧接着就会调用invokeDelegate
,该方法的作用就是让代理过滤器(FilterChainProxy
)去执行doFilter
方法,其源码如下:
从上图源码可知,在doFilter
方法中调用了doFilterInternal
方法,该方法源码如下:
在源码中:
List<Filter> filters = getFilters(fwRequest);
该句代码的意思就是将Spring Security
中所有的过滤器全部加载到过滤器链中。这样就把所有的过滤器加载进来了
总结:
6 认证
6.1 简介
==思考:==
通过之前的HelloWorld例子知道用户名为user
,密码则是启动时随机产生的一段加密字符串
但是在开发中,用户名密码都需要自定义或者从数据库表查询账号跟密码,那么这些在操作
在SpringSecurity
中如何实现?
实现上述问题一共有三种方式:
- 通过配置文件
- 通过配置类
- 自定义实现类
接下来就让挨个来实现这三种方式
6.2 配置文件
-
创建
SpringBoot
项目(spring-security-config-file
),并且导入Spring Security
依赖,pom.xml
部分内容如下:<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.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies>
-
通过配置指定用户名密码
server.port=8081 spring.security.user.name=lisi spring.security.user.password=123456
-
创建Controller,内容如下:
package com.briup.security.web; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/file") public class ConfigFileController { @GetMapping("/hello") public String hello() { return "Hello Security"; } }
-
启动并且进行测试
启动后并没有给我们产生密码,这是因为制定了用户名密码,所以就不会产生密码
访问地址: http://127.0.0.1:8081/config/hello
输入配置文件配置的用户名密码即可看到返回的结果,如下:
注意:这种方式只能用在学习阶段,真正开发项目不会用这个
6.3 配置类
-
创建
SpringBoot
项目(spring-security-config-class
),并且导入Spring Security
依赖,pom.xml
部分内容如下:<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.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies>
-
创建配置类
SecurityConfig
,内容如下:package com.briup.security.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; /** * Security 配置类 必须继承 WebSecurityConfigurerAdapter * 同时必须加上 @Configuration注解 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 重写该方法,并且通过 auth 参数设置用户名密码 * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 对密码进行加密 String password = passwordEncoder().encode("123456"); // 设置用户名与密码 以及 角色 由于这里只是学习,没有用户名和密码, // 因此直接写死为admin auth.inMemoryAuthentication() .withUser("lisi") .password(password) .roles("adimin"); } /** * 配置加密 解密实例 * @return */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
了解内容(start)
PasswordEncoder
接口 是Security
提供用来对密码进行加密的接口,源码如下:BCryptPasswordEncoder
是该接口的实现类,使用算法将接口三个方法全部实现因此使用该实现类的实例就可以对密码进行加密
了解内容(end)
-
创建Controller,内容如下:
package com.briup.security.web; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/class") public class ConfigFileController { @GetMapping("/hello") public String hello() { return "Hello Security"; } }
-
启动测试
注意:将项目端口设置为8082
访问地址:http://127.0.0.1:8082/class/hello
输入配置类配置好的用户名密码即可看到返回的结果,如下:
注意:这种方式在实际开发项目中也不会用到,只用作学习阶段
6.4 自定义
-
简介
实际开发中,更多的是用户名密码甚至包括角色是从数据库中查询出来,而且在登录的时候会有一些用户自定义的逻辑存在,例如 判断账号的状态等等
但是上述两种方式,用户均不可以添加自定义逻辑,认证走的都是
Security
本身的那一套逻辑,因此急需要一套用户可以自己定义认证逻辑的流程。在
Spring Security
中就要想自定义逻辑,只需要实现UserDetailsSerivice
接口即可 -
准备工作
-
新建账号表,存储账号数据,以便认证时用户名密码从表中查询
-- ---------------------------- -- Table structure for account -- ---------------------------- DROP TABLE IF EXISTS `account`; CREATE TABLE `account` ( `id` bigint(20) NOT NULL COMMENT '主键', `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名', `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of account -- ---------------------------- INSERT INTO `account` VALUES (1, 'lisi', '123321');
-
创建
SpringBoot
项目(spring-security-config-account
),pom.xml内容如下:<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <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>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <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> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies>
数据操作框架为
Spring Data JPA
-
application.yml
在
src/main/resources
下新增application.yml
,内容如下:server: port: 9999 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver # 数据库地址 url: jdbc:mysql://172.16.0.154:3306/test?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8 username: root password: root
-
启动类内容如下:
@SpringBootApplication public class SpringSecurityConfigAccountApplication { public static void main(String[] args) { SpringApplication.run(SpringSecurityConfigAccountApplication.class, args); } /* 加密实例 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
-
POJO类 内容如下:
package com.briup.security.bean; import lombok.Data; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import java.io.Serializable; @Data @Table(name = "account") @Entity public class Account implements Serializable { @Id private Long id; private String username; private String password; }
-
DAO层 内容如下
package com.briup.security.dao; import com.briup.security.bean.Account; import org.springframework.data.jpa.repository.JpaRepository; public interface AccountDao extends JpaRepository<Account,Long> { Account findByUsername(String username); }
-
-
服务层开发
要想让
Spring Security
走自定义登录逻辑流程,就只需要实现UserDetailsService
接口,然后通过配置类进行指定即可。创建
MyDetailService
类,内容如下:package com.briup.security.service; import com.briup.security.bean.Account; import com.briup.security.dao.AccountDao; import org.springframework.beans.factory.annotation.Autowired; 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.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.util.Objects; @Service("myDetailService") public class MyDetailService implements UserDetailsService { @Autowired private AccountDao accountDao; @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Account account = accountDao.findByUsername(username); if (Objects.isNull(account)) { throw new UsernameNotFoundException("用户名不存在"); } User user = new User(account.getUsername(), passwordEncoder.encode(account.getPassword()), AuthorityUtils.createAuthorityList("admin")); return user; } }
代码解释:
之所以实现这个接口,是因为
SpringSecurity
默认走的登录逻辑流程就是UserDetailsService
接口实现类对象的登录逻辑,从下图可以看出该接口的实现类有多个如果用户不实现该接口,那么登录逻辑默认就是其他实现类实例的登录逻辑
返回的
UserDetails
,该接口主要包含一些用户信息,其部分源码如下:public interface UserDetails extends Serializable {} /** * 返回获取用户的所有权限 */ Collection<? extends GrantedAuthority> getAuthorities(); /** * 返回取用户密码 */ String getPassword(); /** * 返回获取 */ String getUsername(); /** * 判断账户是否为过期 */ boolean isAccountNonExpired(); /** * 判断账户是被否锁定 */ boolean isAccountNonLocked(); /** * 凭证(密码) 是否过期 */ boolean isCredentialsNonExpired(); /** * 账户是否禁用 */ boolean isEnabled(); }
这是一个接口,因此最后返回其实现类对象
User
如下图:由于设计表时,并没有设计账户权限和是否过期等等,因此全部设置为
null
,权限集合为admin
注意:虽然上图中的User对象,只设置了
User
对象的用户名,密码以及角色权限,但是查看其构造器源码,在源码中设置了帮助用户设置了其他权限,如下 -
配置
新建
SecurityConfig
配置类,内容如下:package com.briup.security.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; 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.password.PasswordEncoder; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired @Qualifier("myDetailService") private UserDetailsService userDetailsService; @Autowired private PasswordEncoder passwordEncoder; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { /** * 设置认证逻辑为用户自定义认证逻辑 * 设置密码加密处理器为 BCryptPasswordEncoder */ auth.userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder); } }
-
web层开发
新建
Controller
,内容如下:package com.briup.security.web; 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"; } }
-
测试
当输入的账号与密码错误时,则直接报错