【SpringSecurity】2. 初学SpringSecurity

专栏目录
未完待续…

SpringSecurity

SpringSecurity是Spring提供的一个安全框架。

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring的IoC(控制反转),DI(依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

1. 相关地址

官方网站

2. 实现认证

  1. 客户端发送请求后,呗身份验证过滤器拦截
  2. 身份验证过滤器调用身份验证管理器
  3. 管理器调用相关的程序,包括用户详情服务、密码编码器,实现身份的验证
  4. 将结果返回给管理器
  5. 管理器再返回给过滤器
  6. 过滤器根据逻辑选择拦截或者将用户信息存入安全性上下文
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. 基于数据库的认证

真正的系统的用户信息存放在数据库中,所以用户认证的流程应该是:

  1. 通过用户名从数据库中取出用户信息
  2. 对用户信息进行校验判断
  3. 将通过校验的用户信息放入定义的安全用户类中

数据库中至少要存在五种表:用户、角色、用户-角色,权限,角色-权限

省略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进行用户安全认证授权的步骤如下:
    1. 首先要实现对数据库的CURD操作,便于查找相关数据
    2. 编写安全用户类(8.1创建安全用户类),用于在获取用户信息时,返回该对象
    3. 编写安全用户服务(8.2创建安全用户服务),在这里通过传入的用户名,实现对安全用户类的实例话,同时权限的授权也在这个类的方法中实现
    4. 编写安全配置类(8.3创建安全配置类),通过这个类可以对SpringSecurity的请求设置权限限制,同时在这个类中一定要配置密码编码器
  • 20
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值