
Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的成员。Spring Security 基于 Spring 框架,提供了一套Web 应用安全性的完整解决方案。
正如你可能知道的关于安全方面的两个主要区域是“认证”和“授权”(或者访问控
制),一般来说,Web 应用的安全性包括**用户认证(Authentication)和用户授权**
(Authorization两个部分,这两点也是 Spring Security 重要核心功能。
-
(1)用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问 该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认 证过程。通俗点说就是系统认为用户是否能登录
-
(2)用户授权指的是验证某个用户是否有权限执行某个 *** 作。在一个系统中,不同用户 所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以 进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的 权限。通俗点讲就是系统判断用户是否有权限去做某些事情
Spring 技术栈的组成部分
通过提供完整可扩展的认证和授权支持保护你的应用程序。 https://spring.io/projects/spring-security
SpringSecurity 特点:
-
和 Spring 无缝整合。
-
全面的权限控制。
-
专门为 Web 开发而设计。
-
旧版本不能脱离 Web 环境使用。
-
新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独 引入核心模块就可以脱离 Web 环境。
-
-
重量级。
Apache 旗下的轻量级权限控制框架。
特点:
-
轻量级。Shiro 主张的理念是把复杂的事情变简单。针对对性能有更高要求 的互联网应用有更好表现。
-
通用性。
- 好处:不局限于 Web 环境,可以脱离 Web 环境使用。
- 缺陷:在 Web 环境下一些特定的需求需要手动编写代码定制。
1.2.3 对比总结
Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之 前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直 是 Shiro 的天下。
相对于 Shiro,在 SSM 中整合 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 入门案例 2.1 入门案例演示- 创建一个项目
- 添加依赖
org.springframework.boot spring-boot-starterorg.springframework.boot spring-boot-starter-testtest org.springframework.boot spring-boot-starter-weborg.springframework.boot spring-boot-starter-security
- 编写Controller代码
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/hello")
public String hello(){
return "hello security!";
}
}
-
在applicationContext.xml中添加tomcat端口配置:server.port=8081
-
访问该项目
默认的用户名:user
密码在项目启动的时候在控制台会打印,注意每次启动的时候密码都回发生变化!
访问成功!!!
2.5 SpringSecurity 基本原理SpringSecurity 本质是一个过滤器链
从启动是可以获取到过滤器链
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFil ter org.springframework.security.web.context.SecurityContextPersistenceFilter org.springframework.security.web.header.HeaderWriterFilter org.springframework.security.web.csrf.CsrfFilter org.springframework.security.web.authentication.logout.LogoutFilter org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter org.springframework.security.web.savedrequest.RequestCacheAwareFilter org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter org.springframework.security.web.authentication.AnonymousAuthenticationFilter org.springframework.security.web.session.SessionManagementFilter org.springframework.security.web.access.ExceptionTranslationFilter org.springframework.security.web.access.intercept.FilterSecurityInterceptor
代码底层流程:重点看三个过滤器:
- FilterSecurityInterceptor:是一个方法级的权限过滤器, 基本位于过滤链的最底部。
super.beforeInvocation(fi) 表示查看之前的 filter 是否通过。 fi.getChain().doFilter(fi.getRequest(), fi.getResponse());表示真正的调用后台的服务。
- ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常
- UsernamePasswordAuthenticationFilter :对/login 的 POST 请求做拦截,校验表单中用户 名,密码。
过滤器加载过程图解:
2.6 UserDetailsService 接口讲解当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。
而在实际项目中 账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。 如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下:
-
返回值 UserDetails
这个类是系统默认的用户“主体”
// 表示获取登录用户所有权限
Collection extends GrantedAuthority> getAuthorities();
// 表示获取密码
String getPassword();
// 表示获取用户名
String getUsername();
// 表示判断账户是否过期
boolean isAccountNonExpired();
// 表示判断账户是否被锁定
boolean isAccountNonLocked();
// 表示凭证{密码}是否过期
boolean isCredentialsNonExpired();
// 表示当前用户是否可用
boolean isEnabled();
以下是 UserDetails 的继承实现关系.
以后我们只需要使用 User 这个实体类即可
-
方法参数 username
表示用户名。此值是客户端表单传递过来的数据。默认情况下必须叫 username,否则无 法接收。
接口实现类:
-
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析 器。
-
BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单 向加密。可以通过 strength 控制加密强度,默认 10.
查用方法演示
@Test
public void test01(){
// 创建密码解析器
BCryptPasswordEncoder bCryptPasswordEncoder = new
BCryptPasswordEncoder();
// 对密码进行加密
String atguigu = bCryptPasswordEncoder.encode("atguigu");
// 打印加密之后的数据
System.out.println("加密之后数据:t"+atguigu);
//判断原字符加密后和加密之前是否匹配
boolean result = bCryptPasswordEncoder.matches("atguigu", atguigu);
// 打印比较结果
System.out.println("比较结果:t"+result);
}
2.8 SpringBoot 对 Security 的自动配置
https://docs.spring.io/spring-security/site/docs/5.3.4.RELEASE/reference/html5/#servlet-hello
3.SpringSecurity Web 权限方案 3.1 设置登录系统的账号、密码- 方式一:在 application.properties
spring.security.user.name=lxy spring.security.user.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("lucy").password(password).roles("admin");
}
@Bean //向Spring容器中加入BCryptPasswordEncoder组件,因为在登录过程中要使用...
PasswordEncoder password(){
return new BCryptPasswordEncoder();
}
}
- 方式三:自定义实现类设置
自定义一个类,实现UserDetailsService,在loadUserByUsername方法中设置登录的配置.
@Service
public class MyUserDetailsService implements UserDetailsService {
//User是UserDetails接口的一个实现类..
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//授权
List auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User("tom",new BCryptPasswordEncoder().encode("123456"),auths);
}
}
在配置类中,设置并加载对应的实现类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//绑定UserDetialService实现类,并对密码进行解码...
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
@Bean //向Spring容器中加入BCryptPasswordEncoder组件,因为在登录过程中要使用...
PasswordEncoder password(){
return new BCryptPasswordEncoder();
}
}
三种方式的图解:
3.2 实现数据库认证来完成用户登录思路图解:
实现步骤:
- 建立数据表
- 添加依赖
org.springframework.boot spring-boot-starterorg.springframework.boot spring-boot-starter-testtest org.springframework.boot spring-boot-starter-webcom.baomidou mybatis-plus-boot-starter3.4.3 mysql mysql-connector-java5.1.49 org.projectlombok lombokorg.springframework.boot spring-boot-starter-security
- 创建实体类:
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Users {
private Integer id;
private String username;
private String password;
}
- 创建UsersMapper接口
@Mapper //@Mapper:在接口类上添加了@Mapper在运行时,通过动态代理生成接口实现类 @Repository //spring的注解,将其加载到spring容器中.. public interface UsersMapper extends baseMapper{ }
- 编写登录实现类
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
//User是UserDetails接口的一个实现类..
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//编写查询条件
QueryWrapper wrapper = new QueryWrapper <>();
//where username=?
wrapper.eq("username",username);
Users users = usersMapper.selectOne(wrapper);
//判断
if(users==null){//数据库没有用户名,认证失败
throw new UsernameNotFoundException("用户名不存在!");
}
//授权
List auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
//当输入时,会使用这个用户名的密码和用户输入的进行匹配,如果不能完全匹配也无法进行访问.
return new User(users.getUsername(),new BCryptPasswordEncoder().encode(users.getPassword()),auths);
}
}
- 在yaml文件中编写数据库配置信息
server:
port: 8081
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///demo
username: root
password: 186259
- 进行测试:
当输入错误的用户名和密码时:
当输入正确的用户名和密码时:
3.3 自定义用户登录界面- 在SecurityConfig重写参数为http的config方法
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
......
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()//自定义自己编写的登录界面.
.loginPage("/login.html")//登录页面设置
.loginProcessingUrl("/user/login")//登录访问路径(controller)
.defaultSuccessUrl("/test/index").permitAll()//登陆成功后,跳转路径
.and().authorizeRequests()//对请求的页面进行授权..
.antMatchers("/","/test/hello","/user/login").permitAll()//设置哪些路径可以直接访问,不需要认证..
.anyRequest().authenticated()//除上面外的所有请求全部需要鉴权认证
.and().csrf().disable();//关闭csrf防护.
}
}
在resources下新建一个static文件夹,编写login.html
Title
注意:页面提交方式必须为 post 请求,所以上面的页面不能使用,用户名,密码必须为 username,password
原因: 在执行登录的时候会走一个过滤器 UsernamePasswordAuthenticationFilter
底层源码:
- 在controller中编写index的方法
@GetMapping("index")
public String index()
{
return "hello index";
}
- 进行测试:
如果直接访问http://localhost:8081/test/hello则不需要进行登录.
如果访问http://localhost:8081/test/index则需要先跳转到登录页面,认证成功后才可以进行访问.
3.4 基于角色或权限进行访问控制 hasAuthority 方法如果当前的主体具有指定的权限,则返回 true,否则返回 false
**修改配置类:**也就是页面被访问需要的权限
修改service:用户所拥有的权限.
hasAnyAuthority 方法如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回 true
修改SecurityConfig代码
hasRole 方法如果用户具备给定角色就允许访问,否则出现 403。
如果当前主体具有指定的角色,则返回 true。
底层代码:
进行演示:
修改配置文件
注意配置文件中不需要添加”ROLE_“,因为上述的底层代码会自动添加与之进行匹配。
hasAnyRole 方法表示用户具备任何一个条件都可以访问。
总结: 3.5 自定义403页面 3.6 注解使用 @Secured判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“。 使用注解先要开启注解功能!
@EnableGlobalMethodSecurity(securedEnabled=true)
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SpringSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityApplication.class, args);
}
}
编写UserController代码:
@GetMapping("update")
@Secured({"ROLE_sale","ROLE_manager"})
public String update()
{
return "hello update!";
}
UserService里面的授权:
//授权 Listauths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale");
可以访问成功!
@PreAuthorize先开启注解功能: @EnableGlobalMethodSecurity(prePostEnabled = true) @PreAuthorize:注解适合**进入方法前的权限验证**, @PreAuthorize 可以将登录用 户的 roles/permissions 参数传到方法中。
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class SpringSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityApplication.class, args);
}
}
#####################Controller###################################
@GetMapping("update")
// @Secured({"ROLE_sale","ROLE_manager"})
@PreAuthorize("hasAnyAuthority('sale')")
public String update()
{
return "hello update!";
}
################Service##########################################
//授权
List auths = AuthorityUtils.commaSeparatedStringToAuthorityList("sale");
@PostAuthorize
先开启注解功能: @EnableGlobalMethodSecurity(prePostEnabled = true) @PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值 的权限.
@RequestMapping("/update")
@ResponseBody
@PostAuthorize("hasAnyAuthority('sale')")
public String update(){
System.out.println("update......");
return "hello update";
}
@PostFilter
@PostFilter :权限验证之后对数据进行过滤 留下用户名是 admin1 的数据
表达式中的 filterObject 引用的是方法返回值 List 中的某一个元素
@RequestMapping("getAll")
@PreAuthorize("hasRole('ROLE_管理员')")
@PostFilter("filterObject.username == 'admin1'")
@ResponseBody
public List getAllUser(){
ArrayList list = new ArrayList<>();
list.add(new UserInfo(1l,"admin1","6666"));
list.add(new UserInfo(2l,"admin2","888"));
return list;
}
@PreFilter
@PreFilter: 进入控制器之前对数据进行过滤
对List的UserInfo,符合id为偶数的将其过滤出来.
@RequestMapping("getTestPreFilter")
@PreAuthorize("hasRole('ROLE_管理员')")
@PreFilter(value = "filterObject.id%2==0")
@ResponseBody
public List getTestPreFilter(@RequestBody List
list){
list.forEach(t-> {
System.out.println(t.getId()+"t"+t.getUsername());
});
return list;
}
3.7 用户注销
在登录页面添加一个退出连接
编写success.html
Title
登录成功!
退出
进行配置:
测试:
3.7 自动登录 原理分析:源码查看:
- 将Token写入浏览器cookie的底层:UsernamePasswordAuthenticationFilter
可以看出底层本质是:创建一个包含用户信息的Token字符串,并将其存入到cookie中.
- 将Token写入数据库的底层原理:jdbcTokenRepositoryImpl
- 实现自动登录的底层原理:RememberMeAuthenticationFilter
- 创建存储Token数据库的表
CREATE TABLE persistent_logins (username VARCHAR(64) NOT NULL, series VARCHAR(64) PRIMARY KEY, token VARCHAR(64) NOT NULL, last_used TIMESTAMP NOT NULL)
- 编写配置类:
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
//注入数据源
@Autowired
private DataSource dataSource;
//配置对象 PersistentTokenRepository是jdbcTokenRepository的上层接口,这里使用了多态
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
@Bean //向Spring容器中加入BCryptPasswordEncoder组件,因为在登录过程中要使用...
PasswordEncoder password(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置退出功能 退出之后进入/test/hello页面
http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll();
//配置没有权限访问然后跳转到自定义页面
http.exceptionHandling().accessDeniedPage("/unauth.html");
http.formLogin()//自定义自己编写的登录界面.
.loginPage("/login.html")//登录页面设置
.loginProcessingUrl("/user/login")//登录访问路径(controller)
.defaultSuccessUrl("/success.html").permitAll()//登陆成功后,跳转路径
.and().authorizeRequests()//对请求的页面进行授权..
.antMatchers("/","/test/hello","/user/login").permitAll()//设置哪些路径可以直接访问,不需要认证..
//hasAuthority方法
// .antMatchers("/test/index").hasAuthority("admins")
//hasAnyAuthority方法
//.antMatchers("/test/index").hasAnyAuthority("admins,manager")
// hasRole方法
// .antMatchers("/test/index").hasRole("sale")
.anyRequest().authenticated()//除上面外的所有请求全部需要鉴权认证
// ############以下是自动登录的配置#####################################
.and().rememberMe().tokenRepository(persistentTokenRepository()) //设置使用的数据库 *** 作方式
.tokenValiditySeconds(60)//设置有效时长,单位秒
.userDetailsService(userDetailsService)//设置采用的自定义userDetailsService
.and().csrf().disable();//关闭csrf防护.
}
}
- 修改login.html的代码:加入自动登录的框框.
Title
测试结果:
登录时选中,自动登录,可以看到浏览器的cookie中有了一个remember-me的cookie.
关闭浏览器后,直接进行访问可以访问.
每一次重新登录,数据库总就会插入一条语句,等下次登录时就会从拿着浏览器的Token和数据库里的进行比对,如果比对上了,说明已经登录…
4.SpringSecurity 微服务权限方案 4.1 什么是微服务 1、微服务由来微服务最早由 Martin Fowler 与 James Lewis 于 2014 年共同提出,微服务架构风格是一种 使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量 级机制通信,通常是 HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制 来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限 度的集中式管理。
2、微服务优势(1)微服务每个模块就相当于一个单独的项目,代码量明显减少,遇到问题也相对来说比 较好解决。
(2)微服务每个模块都可以使用不同的存储方式(比如有的用 redis,有的用 mysql 等),数据库也是单个模块对应自己的数据库。
(3)微服务每个模块都可以使用不同的开发技术,开发模式更灵活。
3、微服务本质(1)微服务,关键其实不仅仅是微服务本身,而是系统要提供一套基础的架构,这种架构 使得微服务可以独立的部署、运行、升级,不仅如此,这个系统架构还让微服务与微服务 之间在结构上“松耦合”,而在功能上则表现为一个统一的整体。这种所谓的“统一的整 体”表现出来的是统一风格的界面,统一的权限管理,统一的安全策略,统一的上线过 程,统一的日志和审计方法,统一的调度方式,统一的访问入口等等。
(2)微服务的目的是有效的拆分应用,实现敏捷开发和部署。
4.2 微服务认证与授权实现思路 1、认证授权过程分析(1)如果是基于 Session,那么 Spring-security 会对 cookie 里的 sessionid 进行解析,找 到服务器存储的 session 信息,然后判断当前用户是否符合请求的要求。
(2)如果是 token,则是解析出 token,然后将当前请求加入到 Spring-security 管理的权限 信息中去
如果系统的模块众多,每个模块都需要进行授权与认证,所以我们选择基于 token 的形式 进行授权与认证.
- 用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限 值,并以用户名为 key,权限列表为 value 的形式存入 redis 缓存中,根据用户名相关信息 生成 token 返回.
- 浏览器将 token 记录到 cookie 中,每次调用 api 接口都默认将 token 携带 到 header 请求头中.
- Spring-security 解析 header 头获取 token 信息,解析 token 获取当前 用户名,根据用户名就可以从 redis 中获取权限列表,这样 Spring-security 就能够判断当前 请求是否有权限访问
具体内容,参考资料中的POM文件
6. 启动Redis和Nacos 7. 编写Commons工具类在service_base中创建相关的工具类,参考资料中的内容…
4.3 案例代码实现 4.3.1 创建认证授权相关的工具类(1)DefaultPasswordEncoder:密码处理的方法
//密码的编码器
@Component
public class DefaultPasswordEncoder implements PasswordEncoder {
public DefaultPasswordEncoder() {
this(-1);
}
public DefaultPasswordEncoder(int length) {
}
//进行MD5加密
@Override
public String encode(CharSequence charSequence) {
return MD5.encrypt(charSequence.toString());
}
//进行密码比对
@Override
public boolean matches(CharSequence charSequence, String encodedPassword) {
//MD5加密两次便是解密..
return encodedPassword.equals(MD5.encrypt(charSequence.toString()));
}
}
(2)TokenManager:token *** 作的工具类
//Token的相关方法.
@Component
public class TokenManager {
//token有效时长
private long tokenExpiration = 24*60*60*1000;
//编码秘钥
private String tokenSignKey = "123456";
//使用jwt根据用户名生成token
public String createToken(String username){
String token = Jwts.builder().setSubject(username)//设置实体
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))//设置过期时间
.signWith(SignatureAlgorithm.HS512,
tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();//设置秘钥
return token;
}
//2.根据token字符串得到用户信息
public String getUserInfoFromToken(String token){
String userinfo = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
return userinfo;
}
//3.删除token 这个方法可写可不写,删除了的话,我们在请求头不带就行了,为必要真的删除.
public void removeToken(String token)
{
}
}
(3)TokenLogoutHandler:退出实现
//退出的处理器
@Component
public class TokenLogoutHandler implements LogoutHandler {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
//这里的赋值是从Spring容器中直接拿对象进行赋值..
public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
//1.从header里面获取token
//2.token不为空,移除token,从redis删除token
String token = request.getHeader("token");
if (token!=null){
//移除
tokenManager.removeToken(token);
//从token获取用户名
String username = tokenManager.getUserInfoFromToken(token);
redisTemplate.delete(username);
}
//响应给客户端成功信息.
ResponseUtil.out(response, R.ok());
}
}
(4)UnauthorizedEntryPoint:未授权统一处理
//认证失败的处理方式 当认证失败时候会调用该方法..
public class UnauthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}
4.3.2 创建认证授权实体类:
SecurityUser类
@Data
public class SecurityUser implements UserDetails {
//当前登录用户
private transient User currentUserInfo;
//当前权限
private List permissionValueList;
public SecurityUser() {
}
public SecurityUser(User user) {
if (user != null) {
this.currentUserInfo = user;
}
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
Collection authorities = new ArrayList <>();
for(String permissionValue : permissionValueList) {
if(StringUtils.isEmpty(permissionValue)) continue;
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
return authorities;
}
@Override
public String getPassword() {
return currentUserInfo.getPassword();
}
@Override
public String getUsername() {
return currentUserInfo.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
User类
@Data
@ApiModel(description = "用户实体类")
public class User implements Serializable {
public static final long serialVersionUID = 1L;
@ApiModelProperty(value = "微信openid")
private String username;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "昵称")
private String nickName;
@ApiModelProperty(value = "用户头像")
private String salt;
@ApiModelProperty(value = "用户签名")
private String token;
}
4.3.3 创建认证和授权的 filter
(1)TokenLoginFilter:认证的 filter
//Token登录的过滤器
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
private AuthenticationManager authenticationManager;
public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
this.authenticationManager = authenticationManager;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
this.setPostOnly(false);
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login", "POST"));
}
//1.获取表单提交用户名和密码
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
User user = null;
try {
user = new ObjectMapper().readValue(request.getInputStream(), User.class);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException();
}
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList <>()));
}
//认证成功调用的方法
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//认证成功,得到认证成功之后的用户信息
SecurityUser user = (SecurityUser)authResult.getPrincipal();
//根据用户名生成token
String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());
//吧用户名称和用户权限列表放到redis
redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(),user.getPermissionValueList());
//返回token
ResponseUtil.out(response, R.ok().data("token",token));
}
//3.认证失败调用的信息.
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
ResponseUtil.out(response,R.error());
}
}
(2)TokenAuthenticationFilter:授权 filter
//Token授权的过滤器.
public class TokenAuthFilter extends BasicAuthenticationFilter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenAuthFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
super(authenticationManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//获取当前认证成功用户的权限信息
UsernamePasswordAuthenticationToken authenRequest = getAuthentication(request);
//判断是否有权限信息,放到权限上下文
if (authenRequest!=null){
SecurityContextHolder.getContext().setAuthentication(authenRequest);
}
chain.doFilter(request,response);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request){
//从header中获取token
String token = request.getHeader("token");
if(token!=null){
//从token中获取用户名
String username = tokenManager.getUserInfoFromToken(token);
//从redis获取对应的权限列表
List permissionValueList = (List )redisTemplate.opsForValue().get(username);
Collection authority = new ArrayList <>();
for (String permissionValue : permissionValueList) {
SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue);
authority.add(auth);
}
return new UsernamePasswordAuthenticationToken(username,token,authority);
}
return null;
}
}
4.3.4 Spring Security 的核心配置
Spring Security 的核心配置就是继承 WebSecurityConfigurerAdapter 并注解 @EnableWebSecurity 的配置。这个配置指明了用户名密码的处理方式、请求路径、登录 登出控制等和安全相关的配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
private DefaultPasswordEncoder defaultPasswordEncoder;
private UserDetailsService userDetailsService;
@Autowired
public TokenWebSecurityConfig(TokenManager tokenManager, RedisTemplate redisTemplate, DefaultPasswordEncoder defaultPasswordEncoder, UserDetailsService userDetailsService) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
this.defaultPasswordEncoder = defaultPasswordEncoder;
this.userDetailsService = userDetailsService;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new UnauthEntryPoint())//没有权限访问的处理
.and().csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and().logout().logoutUrl("/admin/acl/index/logout")//退出路径
.addLogoutHandler(new TokenLogoutHandler(tokenManager, redisTemplate)).and()//设置退出处理器
.addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))//自定义登录过滤器和授权过滤器.
.addFilter(new TokenAuthFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic();
}
//调用userDetailsService和密码处理..
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
}
//不进行认证的路径,可以直接进行访问
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/**");
}
}
4.3.5 自定义UserDetailsService
用于数据库 *** 作用户名和密码.
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private PermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询数据
User user = userService.selectByUsername(username);
//判断
if(user == null) {
throw new UsernameNotFoundException("用户不存在");
}
com.rg.entity.User curUser = new com.rg.entity.User();
BeanUtils.copyProperties(user,curUser);
//根据用户查询用户权限列表
List permissionValueList = permissionService.selectPermissionValueByUserId(user.getId());
SecurityUser securityUser = new SecurityUser();
//将user和权限列表保存到SecurityUser中..
securityUser.setCurrentUserInfo(curUser);
securityUser.setPermissionValueList(permissionValueList);
return securityUser;
}
}
4.4 过程图解
5.SpringSecurity 原理总结
5.1 SpringSecurity 的过滤器介绍
SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤 器链的 15 个过滤器进行说明:
(1) WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于 处理异步请求映射的 WebAsyncManager 进行集成。
(2) SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上 下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在 Session 中维护一个用户的安全信 息就是这个过滤器处理的。
(3) HeaderWriterFilter:用于将头信息加入响应中。
(4) CsrfFilter:用于处理跨站请求伪造。
(5)LogoutFilter:用于处理退出登录。
(6)UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中 获取用户名和密码。默认情况下处理来自==/login== 的请求。从表单中获取用户名和密码 时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个 过滤器的 usernameParameter 和 passwordParameter 两个参数的值进行修改。
(7)DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会 配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
(8)BasicAuthenticationFilter:检测和处理 http basic 认证。
(9)RequestCacheAwareFilter:用来处理请求的缓存。
(10)SecurityContextHolderAwareRequestFilter:主要是包装请求对象 request。 (11)AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。
(12)SessionManagementFilter:管理 session 的过滤器
(13)ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。
(14)FilterSecurityInterceptor:可以看做过滤器链的出口。
(15)RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的 remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。
5.2 SpringSecurity 基本流程Spring Security 采取过滤链实现认证与授权,只有当前过滤器通过,才能进入下一个 过滤器:
绿色部分是认证过滤器,需要我们自己配置,可以配置多个认证过滤器。认证过滤器可以 使用 Spring Security 提供的认证过滤器,也可以自定义过滤器(例如:短信验证)。认 证过滤器要在 configure(HttpSecurity http)方法中配置,没有配置不生效。下面会重 点介绍以下三个过滤器:
-
UsernamePasswordAuthenticationFilter 过滤器:该过滤器会拦截前端提交的 POST 方式 的登录表单请求,并进行身份认证。
-
ExceptionTranslationFilter 过滤器:该过滤器不需要我们配置,对于前端提交的请求会 直接放行,捕获后续抛出的异常(主要是后端抛出的)并进行处理(例如:权限访问限制)。
-
FilterSecurityInterceptor 过滤器:该过滤器是过滤器链的最后一个过滤器,根据资源 权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,并 由 ExceptionTranslationFilter 过滤器进行捕获和处理。
认证流程是在 UsernamePasswordAuthenticationFilter 过滤器中处理的,具体流程如下 所示:
5.3.1 UsernamePasswordAuthenticationFilter 源码当前端提交的是一个 POST 方式的登录表单请求,就会被该过滤器拦截,并进行身份认 证。
该过滤器的 doFilter() 方法实现在其抽象父类 AbstractAuthenticationProcessingFilter中,查看相关源码:
上述的 第二 过程调用了 UsernamePasswordAuthenticationFilter 的 attemptAuthentication() 方法,源码如下:
上述的(3)过程创建的 UsernamePasswordAuthenticationToken 是 Authentication 接口的实现类,该类有两个构造器,一个用于封装前端请求传入的未认 证的用户信息,一个用于封装认证成功后的用户信息:
Authentication 接口的实现类用于存储用户认证信息,查看该接口具体定义:
5.3.2 UsernamePasswordAuthenticationFilter 源码源码图解 5.3.3 ProviderManager 源码上述过程中,UsernamePasswordAuthenticationFilter 过滤器的 attemptAuthentication() 方法的(5)过程将未认证的 Authentication 对象传入 ProviderManager 类的 authenticate() 方法进行身份认证。
ProviderManager 的介绍
ProviderManager 是 AuthenticationManager 接口的实现类,该接口是认证相关的核心接 口,也是认证的入口。在实际开发中,我们可能有多种不同的认证方式,例如:用户名+ 密码、邮箱+密码、手机号+验证码等,而这些认证方式的入口始终只有一个,那就是 AuthenticationManager。
在该接口的常用实现类 ProviderManager 内部会维护一个 List列表,存放多种认证方式,实际上这是委托者模式 (Delegate)的应用。每种认证方式对应着一个 AuthenticationProvider, AuthenticationManager 根据认证方式的不同(根据传入的 Authentication 类型判断)委托 对应的 AuthenticationProvider 进行用户认证。
底层源码:
上述认证成功之后的(6)过程,调用 CredentialsContainer 接口定义的 eraseCredentials()方法去除敏感信息.查看UsernamePasswordAuthenticationToken实现的 eraseCredentials() 方法,该方 法实现在其父类中
5.3.4 认证成功/失败处理上述过程就是认证流程的最核心部分,接下来重新回到 UsernamePasswordAuthenticationFilter 过滤器的 doFilter() 方法,查看认证成 功/失败的处理
依赖委托关系:
5.4 SpringSecurity 权限访问流程图解:
5.4.1 ExceptionTranslationFilter 过滤器该过滤器是用于处理异常的,不需要我们配置,对于前端提交的请求会直接放行,捕获后 续抛出的异常并进行处理(例如:权限访问限制)。具体源码如下:
5.4.2 FilterSecurityInterceptor 过滤器FilterSecurityInterceptor 是过滤器链的最后一个过滤器,该过滤器是过滤器链 的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果 访问受限会抛出相关异常,最终所抛出的异常会由前一个过滤器 ExceptionTranslationFilter 进行捕获和处理。具体源码如下:
需要注意,Spring Security 的过滤器链是配置在 SpringMVC 的核心组件 DispatcherServlet 运行之前。也就是说,请求通过 Spring Security 的所有过滤器, 不意味着能够正常访问资源,该请求还需要通过 SpringMVC 的拦截器链。
5.5 SpringSecurity 请求间共享认证信息一般认证成功后的用户信息是通过 Session 在多个请求之间共享,那么 Spring Security 中是如何实现将已认证的用户信息对象 Authentication 与 Session 绑定的进行 具体分析
- 在前面讲解认证成功的处理方法 successfulAuthentication() 时,有以下代码:
-
查 看 SecurityContext 接 口 及 其 实 现 类 SecurityContextImpl , 该 类 其 实 就 是 对 Authentication 的封装:
-
查 看 SecurityContextHolder 类 , 该 类 其 实 是 对 ThreadLocal 的 封 装 , 存 储 SecurityContext 对象
前面提到过,在 UsernamePasswordAuthenticationFilter 过滤器认证成功之 后,会在认证成功的处理方法中将已认证的用户信息对象 Authentication 封装进 SecurityContext,并存入 SecurityContextHolder。
之后,响应会通过 SecurityContextPersistenceFilter 过滤器,该过滤器的位置 在所有过滤器的最前面,请求到来先进它,响应返回最后一个通过它,所以在该过滤器中 处理已认证的用户信息对象 Authentication 与 Session 绑定。
认证成功的响应通过 SecurityContextPersistenceFilter 过滤器时,会从 SecurityContextHolder 中取出封装了已认证用户信息对象 Authentication 的 SecurityContext,放进 Session 中。当请求再次到来时,请求首先经过该过滤器,该过滤 器会判断当前请求的 Session 是否存有 SecurityContext 对象,如果有则将该对象取出再次 放入 SecurityContextHolder 中,之后该请求所在的线程获得认证用户信息,后续的资源访 问不需要进行身份认证;当响应再次返回时,该过滤器同样从 SecurityContextHolder 取出 SecurityContext 对象,放入 Session 中。具体源码如下:
图解:
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)