Bootstrap

SpringSecurity权限控制

目录

1、Spring Security简介

2、Spring Security实现权限

2.1、Spring Security入门

2.1.1、修改pom文件

2.1.2、添加配置类

2.2、用户认证

2.2.1、自定义组件

2.2.2、核心组件

2.2.3、在配置类配置相关认证类

2.2.4、执行流程

2.3、用户权限

2.3.1、修改UserDetailsService实现类

2.3.2、修改登录过滤器

2.3.3、修改token解析器

2.3.4、修改SpringSecurity配置类

2.3.5、给Controller方法加上权限注解

2.4、自定义权限异常


1、Spring Security简介

Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。

正如你可能知道的关于安全方面的两个核心功能是“认证”和“授权”,一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,这两点也是 SpringSecurity 重要核心功能。

(1)用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码,系统通过校验用户名和密码来完成认证过程。

通俗点说就是系统认为用户是否能登录

(2)用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

通俗点讲就是系统判断用户是否有权限去做某些事情。

2、Spring Security实现权限

要对Web资源进行保护,最好的办法莫过于Filter。要想对方法调用进行保护,最好的办法莫过于AOP(面向切面)。而Spring Security进行认证和鉴权的时候,就是利用的一系列的Filter来进行拦截的。

下面是Spring Security过滤器链:

如图所示,一个请求想要访问到API就会从左到右经过蓝线框里的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分是负责异常处理,橙色部分则是负责授权。进过一系列拦截最终访问到我们的API。

这里面我们只需要重点关注两个过滤器即可:

UsernamePasswordAuthenticationFilter负责登录认证,

FilterSecurityInterceptor负责权限授权。

说明:Spring Security的核心逻辑全在这一套过滤器中,过滤器里会调用各种组件完成功能,掌握了这些过滤器和组件你就掌握了Spring Security!这个框架的使用方式就是对这些过滤器和组件进行扩展。

2.1、Spring Security入门

2.1.1、修改pom文件

<dependencies>
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-security</artifactId>
       <version>2.3.6.RELEASE</version>
    </dependency>
<dependencies/>

说明:依赖包(spring-boot-starter-security)导入后,Spring Security就默认提供了许多功能将整个应用给保护了起来:

  • 要求经过身份验证的用户才能与应用程序进行交互
  • 创建好了默认登录表单
  • 生成用户名为user的随机密码并打印在控制台上
  • 等等......

2.1.2、添加配置类

@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

}

随便访问一个我们写好的接口!!

(出现的页面为spring security的默认验证页面),登录的用户名默认为user,密码在项目启动时会在控制台打印,注意每次启动的时候密码都回发生变化!

输入用户名,密码,成功访问到controller方法并返回数据,说明Spring Security默认安全保护生效。

在实际开发中,这些默认的配置是不能满足我们需要的,我们需要扩展Spring Security组件,完成自定义配置,实现我们的项目需求。

2.2、用户认证

用户认证的流程:

以上大部分步骤,spring-security已经给我们完成了,下面是需要我们做的部分:

  • 此处做的登录验证为前后端分离通过请求头是否携带token进行认证

2.2.1、自定义组件

拓展security用户名密码封装对象User

/**
 * spring-security专用实体对象
 */
public class CustomUser extends User {

    /**
     * 我们自己的用户实体对象,要调取用户信息时直接获取这个实体对象。(这里我就不写get/set方法了)
     */
    private SysUser sysUser;

    public CustomUser(SysUser sysUser, Collection<? extends GrantedAuthority> authorities) {
        super(sysUser.getUsername(), sysUser.getPassword(), authorities);
        this.sysUser = sysUser;
    }

    public SysUser getSysUser() {
        return sysUser;
    }

    public void setSysUser(SysUser sysUser) {
        this.sysUser = sysUser;
    }

}

重写通过用户名获取用户信息的方法(userDetailsService中的loadUserByUsername)

这里是通过用户名进行数据库查询

/**
 * 根据用户名得到用户信息
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserService sysUserService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser= sysUserService.getByUserName(username);
        if(sysUser==null){
            throw new UsernameNotFoundException("该用户名不存在");
        }

        if(sysUser.getStatus().longValue()==0){
            throw new RuntimeException("账号已停用!");
        }
        return new CustomUser(sysUser, Collections.emptyList());
    }
}

重写密码校验规则

这里使用md5加密

/**
 * 密码校验器:对输入的密码和数据库中的密码进行比较
 */
@Component
public class CustomMd5PasswordEncoder implements PasswordEncoder {
    /**
     * 将用户输入的密码进行加密处理
     * @param rawPassword
     * @return
     */
    public String encode(CharSequence rawPassword) {
        return DigestUtils.md5DigestAsHex(rawPassword.toString().getBytes());
    }

    /**
     * 用于密码校验
     * @param rawPassword 用户输入的密码
     * @param encodedPassword 通过用户名查询数据库获得的密码
     * @return
     */
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(DigestUtils.md5DigestAsHex(rawPassword.toString().getBytes()));
    }
}

2.2.2、核心组件

编写登录过滤器

继承UsernamePasswordAuthenticationFilter,对用户名密码进行拦截登录校验

/**
 * <p>
 * 登录过滤器,继承UsernamePasswordAuthenticationFilter,对用户名密码进行拦截登录校验
 * </p>
 */
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
    /**
     * 指明校验的页面
     * @param authenticationManager 里面有密码校验的一系列方法:可以理解为校验者
     */
    public TokenLoginFilter(AuthenticationManager authenticationManager) {
        this.setAuthenticationManager(authenticationManager);//设置校验者
        this.setPostOnly(false);
        //指定登录接口及提交方式,可以指定任意路径
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/system/index/login","POST"));
    }

    /**
     * 登录认证
     * @param req
     * @param res
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException {
        try {
            //通过流的方式将请求的对象封装为指定对象
            LoginVo loginVo = new ObjectMapper().readValue(req.getInputStream(), LoginVo.class);
            //将指定对象的用户名和密码封装为Authentication对象
            Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
            //调用authenticate方法完成验证
            Authentication authenticate = this.getAuthenticationManager().authenticate(authenticationToken);
            return authenticate;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * 登录成功
     * @param request
     * @param response
     * @param chain
     * @param auth
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
        //获取验证成功的对象
        CustomUser customUser = (CustomUser) auth.getPrincipal();
        //生成token
        String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());
        //以原生的方式返回token
        Map<String, Object> map = new HashMap<>();
        map.put("token", token);
        ResponseUtil.out(response, Result.success(map));
    }

    /**
     * 登录失败
     * @param request
     * @param response
     * @param e
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException e) throws IOException, ServletException {

        if(e.getCause() instanceof RuntimeException) {
            ResponseUtil.out(response, Result.build(null, 204, e.getMessage()));
        } else {
            ResponseUtil.out(response, Result.build(null, ResultCodeEnum.LOGIN_MOBLE_ERROR));
        }
    }
}

编写token解析器(将认证成功对象传至上下文中)

并且将authentication对象保存至SecurityContext上下文中

/**
 * <p>
 * 认证解析token过滤器
 * </p>
 */
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    public TokenAuthenticationFilter() {

    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        logger.info("uri:"+request.getRequestURI());
        //如果是登录接口,直接放行
        if("/admin/system/index/login".equals(request.getRequestURI())) {
            chain.doFilter(request, response);
            return;
        }

        UsernamePasswordAuthenticationToken authentication = getAuthentication(request);//获取请求的对象封装为spring-security的对象
        //如果对象存在
        if(null != authentication) {
            SecurityContextHolder.getContext().setAuthentication(authentication);//将对象存至SecurityContext(上下文均可使用)
            chain.doFilter(request, response);//对所有资源进行放行
        } else {
            ResponseUtil.out(response, Result.build(null, ResultCodeEnum.LOGIN_MOBLE_ERROR));
        }
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        // token置于header里
        String token = request.getHeader("token");
        logger.info("token:"+token);
        if (!StringUtils.isEmpty(token)) {
            String useruame = JwtHelper.getUsername(token);
            logger.info("useruame:"+useruame);
            if (!StringUtils.isEmpty(useruame)) {
                return new UsernamePasswordAuthenticationToken(useruame, null, Collections.emptyList());
            }
        }
        return null;
    }
}

2.2.3、在配置类配置相关认证类

@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private CustomMd5PasswordEncoder customMd5PasswordEncoder;


    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    //由于博主正在写前后端分离的项目,下面有些不是前后端分离的可以不用加
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护
        http
                //关闭csrf跨站请求伪造
                .csrf().disable()
                // 开启跨域以便前端调用接口
                .cors().and()
                .authorizeRequests()
                // 指定某些接口不需要通过验证即可访问。登陆接口肯定是不需要认证的
                .antMatchers("/admin/system/index/login").permitAll()
                // 这里意思是其它所有接口需要认证才能访问
                .anyRequest().authenticated()
                .and()
                //TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面,这样做就是为了除了登录的时候去查询数据库外,其他时候都用token进行认证。
                .addFilterBefore(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .addFilter(new TokenLoginFilter(authenticationManager()));

        //禁用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 指定UserDetailService和加密器
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(customMd5PasswordEncoder);
    }

    /**
     * 配置哪些请求不拦截
     * 排除swagger相关请求
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/favicon.ico","/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/doc.html");
    }
}

2.2.4、执行流程

下图为当我们访问登录页面时的整个校验流程

2.3、用户权限

想要达到用户权限的管理,我们需要在UserDetailsService实现类中获取到的我们进行用户权限管理的权限数据,并加至security专用对象中进行认证。

2.3.1、修改UserDetailsService实现类

此处将权限数据保存至User对象中

2.3.2、修改登录过滤器

此处通过验证成功将UserDetails对象里面的权限数据存到redis,用于后续的存入SpringSecuritycontext上下文对象中(在构造方法中注明redisTemplate对象,到配置类自动注入)

2.3.3、修改token解析器

此处从redis中通过用户名获取权限数据并封装为上下文专用对象返回

2.3.4、修改SpringSecurity配置类

2.3.5、给Controller方法加上权限注解

2.4、自定义权限异常

    /**
     * 自定义权限异常
     * @param e
     * @return
     * @throws AccessDeniedException
     */
    @ExceptionHandler(AccessDeniedException.class)
    public Result error(AccessDeniedException e) throws AccessDeniedException {
        return Result.build(null).code(204).message("没有权限访问");
    }

;