深入浅出SpringSecurity--权限篇

我正在参加「掘金·启航计划」

引言

SpringSecurity作为一种安全框架,最核心的功能便是认证和授权。上篇我们介绍了SpringSecurity项目的搭建和对用户认证的过程,本篇主要讲述如何使用SpringSecurity相关方法对用户进行授权,实现完整的认证授权方案。

授权的必要性

在平常的系统中,我们要给用户设置不同的权限,虽然一些后台界面不会直接展示给普通用户,但一些请求可能会被获知,这时候,如果不能对一些请求做一些限制,就会对整个系统产生安全风险。因此,我们需要对不同用户进行授权,在过滤器中过滤用户请求。虽然我们可以对某些请求做用户登录的认证

授权流程

  1. 我们需要开启全局安全设置
@EnableGlobalMethodSecurity(prePostEnabled = true)
  1. 在对应请求前加上注解@PreAuthorize,可以加在请求前,也可以加在类上
@GetMapping("/sec")
    //在进入方法前先验证
    @PreAuthorize("hasAnyAuthority('admin')")
    //在方法执行后再验证 适合验证带有返回值的
//    @PostAuthorize("hasAnyAuthority('admin')")

    public String auth(){
        System.out.println(SecurityContextHolder.getContext().getAuthentication());
        return  "验证成功";
    }

可以看到SecurityContextHolder.getContext().getAuthentication()的值中包含了admin的权限,那么是如何添加上去的呢?

我们可以将登录的个人信息以及权限封装进一个类中去,这样就更直观也更容易理解,这里需要注意的是这个封装类需要去继承UserDetails接口

LoginUser类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {

    private User user;


    //存储权限信息
    private List<String> permissions;



    public LoginUser(User user,List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }


    //存储SpringSecurity所需要的权限信息的集合
    @JSONField(serialize = false)
    private List<GrantedAuthority> authorities;

    @Override
    public  Collection<? extends GrantedAuthority> getAuthorities() {
        if(authorities!=null){
            return authorities;
        }
        //把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
        authorities = permissions.stream().
                map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.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
@AllArgsConstructor
@NoArgsConstructor

@TableName(value = "sys_user")
public class User implements Serializable {
    private static final long serialVersionUID = -40356785423868312L;

    /**
     * 主键
     */
    private Long id;
    /**
     * 用户名
     */
    private String userName;
    /**
     * 昵称
     */
    private String nickName;
    /**
     * 密码
     */
    private String password;
    /**
     * 账号状态(0正常 1停用)
     */
    private String status;
    /**
     * 邮箱
     */
    private String email;
    /**
     * 手机号
     */
    private String phonenumber;
    /**
     * 用户性别(0男,1女,2未知)
     */
    private String sex;
    /**
     * 头像
     */
    private String avatar;
    /**
     * 用户类型(0管理员,1普通用户)
     */
    private String userType;
    /**
     * 创建人的用户id
     */
    private Long createBy;
    /**
     * 创建时间
     */
    private Date createTime;
    /**
     * 更新人
     */
    private Long updateBy;
    /**
     * 更新时间
     */
    private Date updateTime;
    /**
     * 删除标志(0代表未删除,1代表已删除)
     */
    private Integer delFlag;

    public User(String userName, String password) {
        this.userName = userName;
        this.password = password;
    }
}

UserDetailsServiceImpl

UserDetailsServiceImpl中,可以加上查询用户权限的过程,这里我们就暂且设定一个admin值

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
   @Autowired
   private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(wrapper);
        //如果查询不到数据就通过抛出异常来给出提示
        if(Objects.isNull(user)){
            throw new RuntimeException("用户名或密码错误");
        }
        //授权 role必须是以下格式 不能为null
        List<String> list = new ArrayList<>();
        list.add("admin");
        return new LoginUser(user,list);
    }
}

封装权限信息

最最最重要的是 将权限封装 到Authentication中

//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser, null,loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);

这样,就简单实现了一个admin用户权限的用户才能访问的接口

RBAC权限模型

上述方案中,我们只简单设定了一个admin用户,在平常开发中我们需要查询一遍数据库,具体可以参考采用RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。 再将

List<String> list = new ArrayList<>();
        list.add("admin");

替换为

List<String> permissionKeyList =  roleMapper.selectPermsByUserId(user.getId());

Config类

SpringSecurity最最核心的地方应当是配置类,可以设置资源开放,权限控制等 在SpringSecurity中,如果我们需要记住用户登录状态,需要将其命名为remember-me以便SpringSecurity获取

<input type="checkbox" name="remember-me">自动登录

下面是一个较为完整的config配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/index.html")  // 登录界面设置
                .loginProcessingUrl("/user/login")  //登录访问路径
                .defaultSuccessUrl("/hello").permitAll() //登录成功之后 跳转路径
                .and().authorizeRequests() //定义哪些资源不被访问
                .antMatchers("/test","/hello").permitAll() //哪些路径可以直接访问 不需要认证
//               .antMatchers("/test/index").hasAuthority("admin")  //admin才可以访问
//               .antMatchers("/test/index").hasAnyAuthority("admin,manager") 有其中一个权限就可以访问     
                // 可以匿名访问的接口 相当于游客可以访问的界面
//                .antMatchers("/user/login","/index.html").anonymous()
//                .antMatchers("/test/index").hasRole("sale")
//                .antMatchers("/sec").authenticated() // 所有请求需要验证
                /*       配置自动登录 界面中 <input type="checkbox" name="remember-me">自动登录
                        .and().rememberMe().tokenRepository(persistentTokenRepository())
                        .tokenValiditySeconds(60) //设置token有效时长
                        .userDetailsService(userDetailsService)*/
                .and().csrf().disable();     //关闭crsf防护 crsf跨站请求伪造

    }



    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

自定义认证和授权异常处理

在SpringSecurity中,假如用户访问了没有权限的接口,则会直接返回403的错误信息,于是我们希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。

  • 在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。

  • 如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。

  • 如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。

  • 所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。

两个自定义异常类

授权类

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        String jsonString = JSON.toJSONString(Result.validateFailed("权限不足"));

        response.setContentType("text/html; charset=UTF-8");
        response.getWriter().write(jsonString);
    }
}

认证类

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        String jsonString = JSON.toJSONString(Result.failed("认证失败请重新登录"));
        response.setContentType("text/html; charset=UTF-8");
        response.getWriter().write(jsonString);
    }
}

添加注入

@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;

@Autowired
private AccessDeniedHandler accessDeniedHandler;

SecurityConfig中添加配置

http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).
        accessDeniedHandler(accessDeniedHandler);

补充

hasRole方法的使用

hasRole与hasAuthorize方法相似,只是在添加权限时需要加上ROLE_的前缀,如

list.add("admin");
list.add("ROLE_user");

使用hasRole注解时,内部会把我们传入的参数拼接上 ROLE_  后再去比较,相当于与Authorize进行了良好的区分

   @GetMapping("/role")
    @PreAuthorize("hasRole('user')")
    public String update(){
        return "test role";
    }

spring security中@PreAuthorize、@PostAuthorize、@PreFilter和@PostFilter四者的区别

@PreAuthorize、@PostAuthorize是基于权限的控制

  • @PreAuthorize可以用来控制一个方法是否能够被调用
  • @PostAuthorize用来对方法执行后再验证权限(很少),一般用来对返回值进行限制 如:@PostAuthorize("returnObject.id%2==0")

而@PreFilter和@PostFilter是对不符合条件的值进行过滤

如 @PostFilter("filterObject.id%2==0")会移除返回值的id不是偶数

当@PreFilter标注的方法拥有多个集合类型的参数时,需要通过@PreFilter的filterTarget属性指定当前@PreFilter是针对哪个参数进行过滤的。 如@PreFilter(filterTarget="ids", value="filterObject%2==0")会对参数为ids不是偶数的值进行过滤

CRSF防护

CSRF(跨站请求伪造) 也被称为:one click attack/session riding,缩写为:CSRF/XSRF。 是一种控制用户在当前已登录的Web应用程序中执行非本意操作的攻击方法,简单来说,就是攻击者通过技术手段欺骗用户访问一个曾经验证过的网站并进行操作,被访问的网站会以为是用户真的在操作而正常运行,利用了浏览器无法保证请求是用户本身自愿发出的。

在SpringSecurity4.0后,默认会启用CSRF防护,主要针对PATCH,POST,PUT,DELETE方法进行防护,

Spring Security实现防护csrf的原理

spring security在认证之后会生成一个csrfToken保存到HttpSession或者Cookie中。之后每次请求到来时,从请求中提取csrfToken,和保存的csrfToken作比较,进而判断当前请求是否合法。主要通过CsrfFilter过滤器来完成。

总结

SpringSecurity拥有全面的权限控制,需要注意的是去实现重要接口比如UserDetailsService重写自己的逻辑,大部分主要的方法观看源码就能大概知道底层的实现逻辑。SpringSecurity就先介绍到这里了,欢迎大家补充~

猜你喜欢

转载自juejin.im/post/7233678805251948603