天天看点

【超详细断点级别讲解 SpringSecurity】项目实战:用户认证、用户授权

一句简洁明了的权限开发的名句:要对Web资源进行保护,最好的办法莫过于Filter;要想对方法调用进行保护,最好的办法莫过于AOP

文章目录

  • ​​1 Spring Security 介绍​​
  • ​​Spring Security 简介​​
  • ​​Spring Security 对比 Apache Shiro​​
  • ​​SpringSecurity 架构​​
  • ​​2 项目实战​​
  • ​​用户认证​​
  • ​​流程分析​​
  • ​​编码​​
  • ​​Debug 分析(超详细)​​
  • ​​用户授权​​

1 Spring Security 介绍

Spring Security 简介

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

Spring Security 对比 Apache Shiro

SpringSecurity 特点:

  • 和 Spring 无缝整合
  • 全面的权限控制
  • 专门为 Web 开发而设计。旧版本不能脱离 Web 环境使用。新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独引入核心模块就可以脱离 Web 环境
  • 重量级

Apache Shiro 特点:

  • 轻量级。Shiro 主张的理念是把复杂的事情变简单。针对对性能有更高要求的互联网应用有更好表现
  • 通用性。好处:不局限于 Web 环境,可以脱离 Web 环境使用。缺陷:在 Web 环境下一些特定的需求需要手动编写代码定制

SpringSecurity 架构

Spring Security进行认证和鉴权的时候,就是利用的一系列的Filter来进行拦截的。如图所示,一个请求想要访问到API就会从左到右经过蓝线框里的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分是负责异常处理,橙色部分则是负责授权。进过一系列拦截最终访问到我们的API。这里面我们只需要重点关注两个过滤器即可:​

​UsernamePasswordAuthenticationFilter​

​​负责登录认证,​

​FilterSecurityInterceptor​

​负责权限授权。

【超详细断点级别讲解 SpringSecurity】项目实战:用户认证、用户授权

2 项目实战

大致流程如下图

【超详细断点级别讲解 SpringSecurity】项目实战:用户认证、用户授权

引入依赖

<!-- Spring Security依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <scope>provided </scope>
</dependency>      

用户认证

流程分析

【超详细断点级别讲解 SpringSecurity】项目实战:用户认证、用户授权

来到 UsernamePasswordAuthenticationFilter 的父类 AbstractAuthenticationProcessingFilter

【超详细断点级别讲解 SpringSecurity】项目实战:用户认证、用户授权

父类中的 doFilter 就是做上述过程的核心方法

【超详细断点级别讲解 SpringSecurity】项目实战:用户认证、用户授权

其中 attemptAuthentication 为主要认证过程,其细节依赖于 AbstractAuthenticationProcessingFilter 的最终子类是否重写这个方法

【超详细断点级别讲解 SpringSecurity】项目实战:用户认证、用户授权

最终调用 Provider 的 authenticate 方法进行认证

【超详细断点级别讲解 SpringSecurity】项目实战:用户认证、用户授权

最后将认证结果放入上下文

【超详细断点级别讲解 SpringSecurity】项目实战:用户认证、用户授权

编码

【超详细断点级别讲解 SpringSecurity】项目实战:用户认证、用户授权

1 自定义:加密处理组件

@Component
public class CustomMd5PasswordEncoder implements PasswordEncoder {

    public String encode(CharSequence rawPassword) {
        return MD5.encrypt(rawPassword.toString());
    }

    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
    }
}      

2 自定义:用户实体类

public class CustomUser extends User {

    /**
     * 我们自己的用户实体对象,要调取用户信息时直接获取这个实体对象
     */
    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;
    }      

3 自定义:根据用户名查用户信息

@Component
public class UserDetailsServiceImpl implements UserDetailsService {

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

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

4 自定义:登录过滤器

/**
 * <p>
 * 登录过滤器,继承UsernamePasswordAuthenticationFilter,对用户名密码进行登录校验
 * </p>
 *
 */
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

    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 authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
            return this.getAuthenticationManager().authenticate(authenticationToken);
        } 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();
        String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());

        Map<String, Object> map = new HashMap<>();
        map.put("token", token);
        ResponseUtil.out(response, Result.ok(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));
        }
    }
}      

工具类 ResponseUtil

public class ResponseUtil {

    public static void out(HttpServletResponse response, Result r) {
        ObjectMapper mapper = new ObjectMapper();
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        try {
            mapper.writeValue(response.getWriter(), r);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}      

5 自定义:认证过滤器

/**
 * <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);
        if(null != authentication) {
            SecurityContextHolder.getContext().setAuthentication(authentication);
            chain.doFilter(request, response);
        } else {
            ResponseUtil.out(response, Result.build(null, ResultCodeEnum.PERMISSION));
        }
    }

    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;
    }
}      

6 自定义:SpringSecurity 配置类

@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启注解功能,默认禁用注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private CustomMd5Password customMd5PasswordEncoder;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private LoginLogService loginLogService;

    @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(redisTemplate), UsernamePasswordAuthenticationFilter.class)
                .addFilter(new TokenLoginFilter(authenticationManager(), redisTemplate,loginLogService));

        //禁用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");
    }
}      

Debug 分析(超详细)

再放一次流程拓扑图

【超详细断点级别讲解 SpringSecurity】项目实战:用户认证、用户授权

发起登录请求

【超详细断点级别讲解 SpringSecurity】项目实战:用户认证、用户授权

首先来到我们自定义的认证过滤器,由于是管理员登录接口的请求路径,因此直接放行,否则就走下面的获取 header 中 token 的方法(从缓存中查此 token 中包含的 useruame 对应的权限列表),校验是否正确并存在

【超详细断点级别讲解 SpringSecurity】项目实战:用户认证、用户授权

之后来到了登录过滤器,创建出 UsernamePasswordAuthenticationToken

【超详细断点级别讲解 SpringSecurity】项目实战:用户认证、用户授权

之后走 authenticate 方法对密码进行加密

【超详细断点级别讲解 SpringSecurity】项目实战:用户认证、用户授权

之后来到之前自定义的根据用户名查用户信息的实现类

【超详细断点级别讲解 SpringSecurity】项目实战:用户认证、用户授权

同时获取了用户权限的相关信息(这里是测试时用的代码,可以删掉)

【超详细断点级别讲解 SpringSecurity】项目实战:用户认证、用户授权

之后才走密码的校验

【超详细断点级别讲解 SpringSecurity】项目实战:用户认证、用户授权

最后来到了登录过滤器的成功响应方法

【超详细断点级别讲解 SpringSecurity】项目实战:用户认证、用户授权

成功获取到 token

【超详细断点级别讲解 SpringSecurity】项目实战:用户认证、用户授权

用户授权

每次请求进来,进入认证过滤器,由于不是登录请求,所以会被拦截判断 header 中的 token 是否正确,以及对应解析出来的用户名中是否在缓存中对应权限列表

【超详细断点级别讲解 SpringSecurity】项目实战:用户认证、用户授权