Springboot——整合SpringSecurity
认证过程主要是实现AuthenticationManager, AuthenticationManager最重要的实现类是ProviderManager, 通过ProviderManager,可以管理AuthenticationProvider, 每个AuthenticationProvider都对应一种认证方式, 当所有认证方式不支持时,调用ProviderManager的parent认证。如果
目录
一、核心概念
SpringSecurity的核心包括认证和授权两个部分。
认证
认证过程主要是实现AuthenticationManager, AuthenticationManager最重要的实现类是ProviderManager, 通过ProviderManager,可以管理AuthenticationProvider, 每个AuthenticationProvider都对应一种认证方式, 当所有认证方式不支持时,调用ProviderManager的parent认证。
SpringSecurity提供的认证其实是一种接口,他允许你自定义认证方式,这种设计更像是一种规范设计。
授权
AccessDecisionVoter 是一个投票器,投票器会检查用户是否具备应有的角色,进而投出赞成、反对或者弃权票;
AccessDecisionManager 则是一个决策器,来决定此次访问是否被允许。
过滤链
开发者所见到的 Spring Security 提供的功能,都是通过这些过滤器来实现的,这些过滤器按照既定的优先级排列,最终形成一个过滤器链。
需要注意的是,默认过滤器并不是直接放在 Web 项目的原生过滤器链中,而是通过一个FilterChainProxy 来统一管理。Spring Security 中的过滤器链通过 FilterChainProxy 嵌入到 Web项目的原生过滤器链中,如下图所示。
二、Springboot整合SpringSecurity核心流程
在将SpringSecurity整合到Springboot之前,我们需要知道整体的流程以便于我们更好的理解验证过程。
- 引入
spring-boot-starter-security
依赖 - 创建
SecurityConfig
配置文件 - 重写
UserDetailsService
第2点中,又可以拆分为登录、登出、错误处理、自动登录、过滤器等配置,如果完全不配置,SpringSecurity也会为你提供一套默认配置,从引入依赖开始就能够正常工作了。
第3点中,重写UserDetailsService.loadUserByUsername(String username)方法可以让你自定义验证的过程。如验证时,你希望通过查询MySQL数据库,如果数据库中存在该用户并且密码验证正确,就返回SpringSecurity中的User对象,这些过程都可以自定义实现。
三、Springboot整合SpringSecurity
3.1 引入依赖
在pom.xml文件中引入依赖后,SpringSecurity在Springboot启动时就能开始正常工作了。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
3.2 创建SecurityConfig配置文件
如果什么都不配置,那么该配置文件的内容应该这样的,剩下的事情SpringSecurity会帮你设置默认配置。
package com.example.demo.config.security;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { }
}
如果想要自定义配置,如自定义登录界面、自定义验证过程,或者想要整合一些工具,如Jwt,这个时候需要在SecurityFilterChain
中配置。
比较简单的配置:
package com.example.demo.config.security;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
// 登录页为/login.html,请求/login路径验证登录信息,成功后重定向到/index.html页
httpSecurity.formLogin()
.loginProcessingUrl("/login")
.loginPage("/login.html")
.defaultSuccessUrl("/index.html");
// 允许登录页和登录的url不需要验证即可访问,因为那个时候还没有登录信息
httpSecurity.authorizeRequests()
.antMatchers("/login*").permitAll()
.anyRequest().authenticated();
httpSecurity.csrf().disable();
return httpSecurity.build();
}
}
更全的配置:
其中,SpringSecurity整合JwtToken可以看另一篇文章。
package com.example.demo.config.security;
/**
* @Author : HuangJiajian
* @create 2022/10/18 17:49
*/
@Configuration
public class SecurityConfig {
@Autowired
UserDetailServiceImpl userDetailService;
@Autowired
PersistentTokenRepository persistentTokenRepository;
@Autowired
private JwtFilter jwtFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
// 登录,自定义登录界面为/login.html,通过请求/login路径验证身份
httpSecurity.formLogin()
.loginProcessingUrl("/login")
.loginPage("/login.html")
.successHandler(new AuthenticationSuccessConfig())
.failureHandler(new AuthenticationFailConfig());
// 退出,自定义退出成功时的操作,如响应的数据
httpSecurity.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new LogoutSuccessConfig());
// 过滤器,除了/test/*和/login*的url请求不需要验证身份外,其他的请求都需要身份验证
httpSecurity.authorizeRequests()
.antMatchers("/test/*").permitAll()
.antMatchers("/login*").permitAll()
.anyRequest().authenticated();
// 错误处理,自定义认证失败的错误处理,如403返回权限不足等
httpSecurity.exceptionHandling()
.accessDeniedHandler(new AccessDeniedConfig())
.authenticationEntryPoint(new AuthenticationEntryPointConfig());
// 整合JwtToken验证身份
httpSecurity.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
httpSecurity.csrf().disable();
return httpSecurity.build();
}
@Bean
public PasswordEncoder getPasswordEncoder() {
// 选择非对称加密为SpringSecurity的加密方法
return new BCryptPasswordEncoder();
}
}
3.3 重写UserDetailsService
Spring Security 中定义了 UserDetails 接口来规范开发者自定义的用户对象,这样方便一些旧系统、用户表已经固定的系统集成到 Spring Security 认证体系中。而负责提供用户数据源的接口是 UserDetailsService, UserDetailsService 中只有一个查询用户的方法,那就是loadUserByUsername。
换句话说,我们可以使用UserDetailsService.loadUserByUsername(String username)
方法来自定义查询用户的过程。这部分的代码,只需要关心loadUserByUsername
的自定义逻辑即可。注意这里的User类是来自于SpringSecurity的,而非自己在Entity中定义的User类。
package com.example.demo.service.impl;
/**
* @Author : HuangJiajian
* @create 2022/10/19 10:03
*/
@Service
@Component
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Autowired
UserRoleMapper userRoleMapper;
@Autowired
RoleMapper roleMapper;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* @Author HJJ
* @Date 2022-12-27 9:43
* @Params String username
* @Return User
* @Description SpringSecurity方法,验证并获取用户权限信息,并将User数据写入Redis缓存
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String cacheUser = stringRedisTemplate.opsForValue().get("username::" + username);
// 如果有缓存,直接返回,如果没有缓存,否则需要查询数据库并写入Redis缓存
if (!Objects.isNull(cacheUser)) {
return JSON.parseObject(cacheUser, UserDetails.class);
} else {
UserEntity userEntity = getUserEntityByUsername(username);
User user = new User(username, userEntity.getUserPassword(), getAuthoritiesByUserId(userEntity.getUserId()));
String strUser = JSONObject.toJSONString(user);
System.out.println(username+"查询了数据库并写入Redis缓存:" + strUser);
// 将User对象转换成String并写入缓存
stringRedisTemplate.opsForValue().set("username::" + username, strUser, 60, TimeUnit.SECONDS);
return user;
}
}
/**
* @Author HJJ
* @Date 2022-12-27 9:42
* @Params
* @Return
* @Description 查询数据库,获取用户信息
*/
public UserEntity getUserEntityByUsername(String username) {
QueryWrapper<UserEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userName", username);
UserEntity userEntity = userMapper.selectOne(queryWrapper);
if (Objects.isNull(userEntity)) {
System.out.println("没有此用户:" + username);
throw new UsernameNotFoundException("没有此用户");
}
return userEntity;
}
/**
* @Author HJJ
* @Date 2022-12-27 9:55
* @Params
* @Return
* @Description 查询数据库,获取用户权限信息
*/
public List<GrantedAuthority> getAuthoritiesByUserId(int userId) {
StringBuilder roles = new StringBuilder();
QueryWrapper<UserRole> queryWrapper1 = new QueryWrapper<>();
queryWrapper1.eq("userId", userId);
List<UserRole> userRoles = userRoleMapper.selectList(queryWrapper1);
for (int i = 0; i < userRoles.size(); i++) {
UserRole userRole = userRoles.get(i);
int roleId = userRole.getRoleId();
QueryWrapper<Role> queryWrapper2 = new QueryWrapper<>();
queryWrapper2.eq("roleId", roleId);
Role role = roleMapper.selectOne(queryWrapper2);
String roleName = role.getRoleName();
if (Objects.equals(userRoles.size() - 1, i)) {
roles.append(roleName);
break;
}
roles.append(roleName).append(",");
}
return AuthorityUtils.commaSeparatedStringToAuthorityList(roles.toString());
}
}
以上即是Springboot整合SpringSecurity的整体过程。
参考文献
- 深入浅出SpringSecurity-王松
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)