SpringSecurity权限控制

目录


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("没有权限访问");
    }

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
心中带点小风骚的头像心中带点小风骚普通用户
上一篇 2023年12月19日
下一篇 2023年12月19日

相关推荐