专栏目录
未完待续…
文章目录
SpringSecurity
SpringSecurity是Spring提供的一个安全框架。
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring的IoC(控制反转),DI(依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
1. 相关地址
2. 实现认证
- 客户端发送请求后,呗身份验证过滤器拦截
- 身份验证过滤器调用身份验证管理器
- 管理器调用相关的程序,包括用户详情服务、密码编码器,实现身份的验证
- 将结果返回给管理器
- 管理器再返回给过滤器
- 过滤器根据逻辑选择拦截或者将用户信息存入安全性上下文
2.1 引入依赖
引入依赖后,启动项目,项目资源就已经在SpringSecurity框架的保护下。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
开发环境中使用的默认用户名(user)和密码会打印在控制台。
2.2 配置用户名和密码
在application.yaml中配置相关信息
spring:
security:
user:
name: admin
password: 123123
2.3 基于内存的多用户管理
将用户信息存储到内存中,实现配置多个用户
使用配置类后,application中配置的信息便失效
新建配置类MySecurityUserConfig
,实现用户详情服务接口
@Configuration
public class MySecurityUserConfig{
@Bean
public UserDetailsService userDetailsService(){
//创建用户1
UserDetails user1=User.builder()
.username("jiabao")
.password("123123")
.roles("student")//定义角色
.build();
//创建用户2
UserDetails user2=User.builder()
.username("yujiabao")
.password("123123")
.roles("teacher")
.build();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
//存入内存
manager.createUser(user1);
manager.createUser(user2);
return manager;
}
//配置密码编码器,必须配置
//PasswordEncoder使用单例模式,不具备密码加密功能
//提供方法encode加密密码,matches进行密码匹配,getInstance得到对象
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
}
3. 密码处理
上文的认证学习中,在实现上存在一下问题:
- 为实现对密码的加密,密码属于明文存储
- 并没有对用户的权限进行管理
- 用户都是写死的,需要实现动态创建
3.1 加密方案
密码加密一般使用散列函数、又称散列算法或哈希函数,属于单向函数,常用的散列算法有MD5和SHA。
SpringSecurity提供了多种密码加密方案,基本实现PasswordEncoder
接口中,官方推荐使用BCryptPasswordEncoder
。
3.2 使用BCryptPasswordEncoder对密码加密
@Configuration
public class MySecurityUserConfig{
@Bean
public UserDetailsService userDetailsService(){
//创建用户1
UserDetails user1=User.builder()
.username("jiabao")
.password(passwordEncoder().encode("123123"))//加密
.roles("student")//定义角色
.build();
//创建用户2
UserDetails user2=User.builder()
.username("yujiabao")
.password(passwordEncoder().encode("123123"))
.roles("teacher")
.build();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
//存入内存
manager.createUser(user1);
manager.createUser(user2);
return manager;
}
//BCryptPasswordEncoder提供方法:
//encode()加密
//match()匹配
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
4. 用户权限
4.1 获取登录用户信息
Authentication接口继承Principal接口
通过登录认证后,访问该接口页面,就可以得到当前用户信息
@RestController
public class CurrentLoginuserController{
//从Authentication获取
@GetMapping("/getUser1")
public Authentication getLoginUser1(Authentication authentication){
return authentication;
}
//从Principal获取
@GetMapping("/getUser2")
public Principal getLoginUser2(Principal principal){
return principal;
}
//从安全上下文获取
@GetMapping("/getUser3")
public Principal getLoginUser3(){
return SecurityContextHolder.getContext().getAuthentication();
}
}
4.2 配置权限
创建用户时给用户配置角色
@Configuration
public class MySecurityUserConfig{
@Bean
public UserDetailsService userDetailsService(){
//创建用户1
UserDetails user1=User.builder()
.username("jiabao")
.password("123123")
.roles("student")//定义角色
.authorities("student:delete","student:add")//配置权限
.build();
...
}
}
构造的时候roles和authorities属于顺序执行关系,即会产生权限覆盖的情况
5. 授权
5.1 针对Controller
权限授权
新建一个WebSecurityConfig
类去继承WebSecurityConfigurerAdapter
类
@Configuration
public WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception{
http.authorizeHttpRequests()//授权http请求
.anyRequest()//任何请求
.denyAll();//拒绝所有
.permitAll();//允许所有
http.formLogin().permitAll();//允许表单登录
// -------- // 没有配置的权限,默认登录成功即可访问
http.authorizeHttpRequests()//授权http请求
.mvcMatchers("/student/**")//进行请求匹配
.hasAnyAuthority("student:add","ROLE_student");//拥有任意权限可以访问
.mvcMatchers("/teacher/**")//进行请求匹配
.hasAnyAuthority("teacher:");//拥有指定权限可以访问
.anyRequest()//任何请求
.authenticated();//都需要登录
}
}
5.2 针对Method
权限授权
针对某一个方法进行权限控制,可以控制任何类中的方法权限
需要在用户安全配置类上添加@EnableGlobalMethodSecurity
启用全局方法安全
@EnableGlobalMethodSecurity(prePostEnable=true)//开启全局方法安全,启用预授权和后授权注解
public WebSecurityConfig extends WebSecurityConfigurerAdapter{
...
}
然后在对应的方法上使用授权注解
public class xxxImpl implements xxxService{
@Override
//预授权:有权限才能访问;后授权:访问后再判断是否有权限
@PreAuthorize("hasAuthority('teacher:add')")//有该权限才能访问该方法
public void addUser(User user){
...
}
}
6. Security返回JSON
在前后端分离业务中,主要通过JSON进行交互,登录失败或成功不会进行页面跳转,而是用JSON进行提示。
6.1 编写返回JSON的对象类
//使用lombok简化代码
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class HttpResult{
private Integer code;//自定义响应码
private String msg;//消息
private Object data;//数据
}
6.2 编写认证成功处理器
只要认证成功,就会调用该接口的方法
@Component
public class AppAutheticationSuccessHandler implements AuthenticationSuccessHandler{
@Resource //注入序列化器,对JSON进行序列化和反序列化
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request
,HttpServletResponse response){
HttpResult httpResult=HttpResult().builder()
.code(1)
.msg("登陆成功")
.build();
//对象转成JSON
String responseJson = objectMapper.writeValueAsString(httpResult);
//设置编码字符集
response.setCharacterEncoding("UTF-8");
response.setContentType("appplicatino/json;charset=utf-8");
//将JSON返回给前端
PrintWriter writer = response.getWriter();
writer.println(responseJson);
writer.flush();
}
}
6.3 编写认证失败处理器
只要认证失败,就会调用该接口的方法
@Component
public class AppAutheticationFailHandler implements AuthenticationFailHandler{
@Resource //注入序列化器,对JSON进行序列化和反序列化
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request
,HttpServletResponse response){
HttpResult httpResult=HttpResult().builder()
.code(0)
.msg("登陆失败")
.build();
//对象转成JSON
String responseJson = objectMapper.writeValueAsString(httpResult);
//设置编码字符集
response.setCharacterEncoding("UTF-8");
response.setContentType("appplicatino/json;charset=utf-8");
//将JSON返回给前端
PrintWriter writer = response.getWriter();
writer.println(responseJson);
writer.flush();
}
}
6.4 退出成功处理器
退出成功后会调用该接口
@Component
public class AppLogoutSuccessHandler implements LogoutSuccessHandler{
@Resource //注入序列化器,对JSON进行序列化和反序列化
private ObjectMapper objectMapper;
@Override
public void onLogoutSuccess(HttpServletRequest request
,HttpServletResponse response){
HttpResult httpResult=HttpResult().builder()
.code(1)
.msg("退出成功")
.build();
//对象转成JSON
String responseJson = objectMapper.writeValueAsString(httpResult);
//设置编码字符集
response.setCharacterEncoding("UTF-8");
response.setContentType("appplicatino/json;charset=utf-8");
//将JSON返回给前端
PrintWriter writer = response.getWriter();
writer.println(responseJson);
writer.flush();
}
}
6.5 访问拒绝处理器
@Component
public class AppAccessDenyHandler implements AccessDenyHandler{
@Resource //注入序列化器,对JSON进行序列化和反序列化
private ObjectMapper objectMapper;
@Override
public void handle(HttpServletRequest request
,HttpServletResponse response){
HttpResult httpResult=HttpResult().builder()
.code(1)
.msg("访问拒绝")
.build();
//对象转成JSON
String responseJson = objectMapper.writeValueAsString(httpResult);
//设置编码字符集
response.setCharacterEncoding("UTF-8");
response.setContentType("appplicatino/json;charset=utf-8");
//将JSON返回给前端
PrintWriter writer = response.getWriter();
writer.println(responseJson);
writer.flush();
}
}
6.6 修改安全配置类
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Resuorce
private AppAutheticationSuccessHandler appAutheticationSuccessHandler;
@Resource
private AppAutheticationFailHandler appAutheticationFailHandler;
@Resource
private AppLogoutSuccessHandler appLogoutSuccessHandler;
@Resource
private AppAccessDenyHandler appAccessDenyHandler;
@Override
protected void configure(HttpSecurity http) throws Exception{
http.authorizeRequests().anyRequest().authenticated();
//设置登录认证处理器
http.formLogin()
.successHandler(appAutheticationSuccessHandler)//登录成功处理器
.failureHandler(appAutheticationFailHandler)//登录失败处理器
.permitAll();
//设置退出处理器
http.forLogout()
.successHandler(appLogoutSuccessHandler);//退出成功处理器
//异常处理器
http.exceptionHandling()
.accessDeniedHandler(appAccessDenyHandler)//访问拒绝处理器
}
}
7. 自定义用户信息类
首先定义一个用户类,然后再定义一个服务类,当服务类处理通过后,就会返回所定义的用户的信息实例。
7.1 自定义安全用户信息类
public class SecurityUser implements UserDetails{
...
}
方法名 | 说明 |
---|---|
getAuthorities(); | 获取当前用户对象所具有的角色信息 |
getPassword(); | 获取当前用户对象的密码 |
getUsername(); | 获取当前用户对象的用户名 |
isAccountNonExpired(); | 当前账户是否未过期 |
isAccountNonLocked(); | 当前账户是否未锁定 |
isCredentialsNonExpired(); | 当前账户密码是否未过期 |
isEnabled(); | 当前账户是否可用 |
7.2 创建用户服务类
@Service
public class UserServiceImpl implements UserDetailsService{
@Override
public UserDetails loadUserByUsername(String username) throws
UsernameNotFundException{
if(!StringUtils.hasText(username)){
throw new UsernameNotFundException("用户不存在");
}
if(!username.equals("name")){
throw new UsernameNotFundException("用户不存在");
}
//用户名正确,返回用户
SecurityUser securityUser = new SecurityUser();
return securityUser;
}
}
8. 基于数据库的认证
真正的系统的用户信息存放在数据库中,所以用户认证的流程应该是:
- 通过用户名从数据库中取出用户信息
- 对用户信息进行校验判断
- 将通过校验的用户信息放入定义的安全用户类中
数据库中至少要存在五种表:用户、角色、用户-角色,权限,角色-权限
省略SQL,Mybatis等数据库CURD操作的实现
从与数据库表对应的用户实体类名称是
SysUser
8.1 创建安全用户类
public class SecurityUser implements UserDetails{
private final SysUser sysUser;
public SecurityUser(){}//如果定义了有参构造,要习惯定义一个无参构造
public SecurityUser(SysUser sysUser){
this.sysUser=sysUser;
}
...
@Override
public String getPassword(){
String passwd = sysUser.getPassword();
sysUser.setPassword(null);//擦除密码,防止传回前端
return passwd;
}
}
8.2 创建安全用户服务
@Service
public class SecurityUserServiceImpl implements UserDetailsService{
@Resource
private SysUserService sysUserService;//mybatis数据库接口
@Override
public UserDetails loadUserByUsername(String username) throws
UsernameNotFundException{
SysUser sysUser = sysUserService.getByUsername(username);
if(null == sysUser){
throw new UsernameNotFundException("用户不存在");
}
//其他判断逻辑
...
//用户名正确,返回用户
SecurityUser securityUser = new SecurityUser(sysUser);
return securityUser;
}
}
8.3 创建安全配置类
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception{
http.authorizeRequests().anyRequest().authenticated();
http.formLogin().permitAll();
}
//密码编码器
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
9. 基于数据库的方法授权
使用多表联查的SQL语句通过用户的UID获取对应权限
省略SQL,Mybatis等数据库CURD操作的实现
9.1 修改安全用户类,设置权限的返回
public class SecurityUser implements UserDetails{
private final SysUser sysUser;
private List<SimpleGrantedAuthority> authorityList;//用于存储权限
public SecurityUser(){}//如果定义了有参构造,要习惯定义一个无参构造
public SecurityUser(SysUser sysUser){
this.sysUser=sysUser;
}
public void setAuthorityList(List<SimpleGrantedAuthority> authorityList){
this.authorityList=authorityList;
}
//用于返回权限
@Override
public Collection<? extends GrantedAuthority> getAuthorities(){
//List中必须放置继承GrantedAuthority的类
return authorityList;
}
...
}
9.2 修改安全用户服务类,获取用户权限
@Service
public class SecurityUserServiceImpl implements UserDetailsService{
@Resource
private SysUserService sysUserService;//mybatis获取用户信息的接口
@Resource
private SysMenuService sysMenuService; //mybatis获取权限的接口
@Override
public UserDetails loadUserByUsername(String username) throws
UsernameNotFundException{
SysUser sysUser = sysUserService.getByUsername(username);
if(null == sysUser){
throw new UsernameNotFundException("用户不存在");
}
//获取用户权限
List<String> userPermissions = sysMenuService
.queryPermissionsByUserId(sysUser.getUserId());
//取出权限实现写法1
List<SimpleGrantedAuthority> authorityList;
for(String userPermission:userPermissions){
SimpleGrantedAuthority simpleGrantedAuthority =
new SimpleGrantedAuthority(userPermission);
authorityList.add(simpleGrantedAuthority);
}
//取出权限实现写法2
List<SimpleGrantedAuthority> authorityList =
userPermissions.stream().map(SimpleGrantedAuthority::new)
.collect(Collect.toList());
//其他判断逻辑
...
//用户名正确,返回用户
SecurityUser securityUser = new SecurityUser(sysUser);
//放入用户权限
securityUser.setAuthorityList(authorityList);
return securityUser;
}
}
10. 总结
- SpringSecurity框架对系统安全提供了很大的支持
- SpringSecurity提供了多种密码加密方案(3.密码处理)
- SpringSecurity可以实现注解式的权限授权(5.授权)
- SpringSecurity可以自定义控制器(6.2编写认证成功处理器),来相应不同操作请求的结果
- 实际开发中用户信息存储在数据库中,那SpringSecurity进行用户安全认证授权的步骤如下:
- 首先要实现对数据库的CURD操作,便于查找相关数据
- 编写安全用户类(8.1创建安全用户类),用于在获取用户信息时,返回该对象
- 编写安全用户服务(8.2创建安全用户服务),在这里通过传入的用户名,实现对安全用户类的实例话,同时权限的授权也在这个类的方法中实现
- 编写安全配置类(8.3创建安全配置类),通过这个类可以对SpringSecurity的请求设置权限限制,同时在这个类中一定要配置密码编码器