SpringSecurity登录验证和鉴权粗解

本文详细介绍了SpringSecurity框架的核心作用,包括认证和授权,以及如何通过WebSecurityConfigurerAdapter进行配置,重点讲解了使用JWT进行token验证和权限控制的流程。
摘要由CSDN通过智能技术生成

一、概念:

SpringSecurity是Spring家族中的一个安全管理框架,它的底层是一系列的拦截器和拦截规则,相当于一个拦截器链

二、核心作用:

认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户

授权:经过认证后判断当前用户是否有权限进行某个操作

三、核心流程图:

四、核心配置接口:WebSecurityConfigurerAdapter

在项目中使用一般定义一个配置类实现该接口,然后即可通过重新其中的配置方法对SpringSecurity相关规则进行配置,示例代码如下:

/** * spring security配置 *  *  */
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**     * 自定义用户认证逻辑     */  
      @Autowired  
        private UserDetailsService userDetailsService;
    
    /**     * 认证失败处理类     */  
      @Autowired 
         private AuthenticationEntryPointImpl unauthorizedHandler;

    /**     * 退出处理类     */   
     @Autowired   
      private LogoutSuccessHandlerImpl logoutSuccessHandler;

    /**     * token认证过滤器     */   
     @Autowired  
       private JwtAuthenticationTokenFilter authenticationTokenFilter;

    /**     * 跨域过滤器     */ 
       @Autowired  
         private CorsFilter corsFilter;
    
    /**     * 解决 无法直接注入 AuthenticationManager    
     *     * @return     
     * @throws Exception     */   
     @Bean   
      @Override  
        public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**     * anyRequest          |   匹配所有请求路径     
    * access              |   SpringEl表达式结果为true时可以访问     
    * anonymous           |   匿名可以访问     
    * denyAll             |   用户不能访问     
    * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)     
    * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问     
    * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问     
    * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问     
    * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问     
    * hasRole             |   如果有参数,参数表示角色,则其角色可以访问     
    * permitAll           |   用户可以任意访问     
    * rememberMe          |   允许通过remember-me登录的用户访问     
    * authenticated       |   用户登录后可访问     */    
    @Override    
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity                // CSRF禁用,因为不使用session            
            .csrf().disable()
                // 认证失败处理类               
                 .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于token,所以不需要session              
                  .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求               
                 .authorizeRequests()
                // 对于登录login 验证码captchaImage 允许匿名访问              
                  .antMatchers("/login", "/captchaImage").anonymous()
                .antMatchers(
                        HttpMethod.GET,
                        "/*.ttf" ).permitAll().
                        antMatchers(
                        //mybatis复习相关的接口全部放行,同学们可以通过postMan进行测试而不需要进行权限认证                      
                          "/review/**", "/review"                ).permitAll()
                .antMatchers("/common/downloadByMinio**").permitAll()
                .antMatchers("/profile/**").anonymous()
                .antMatchers("/common/download**").anonymous()
                .antMatchers("/common/download/resource**").anonymous()
                .antMatchers("/webjars/**").anonymous()
                .antMatchers("/*/api-docs").anonymous()
                .antMatchers("/druid/**").anonymous()
                // 除上面外的所有请求全部需要鉴权认证             
                   .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 添加JWT filter      
          httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加CORS filter       
         httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
    }

    
    /**     * 强散列哈希加密实现     */  
      @Bean   
       public BCryptPasswordEncoder bCryptPasswordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

    /**     * 身份认证接口     */  
      @Override  
        protected void configure(AuthenticationManagerBuilder auth) throws Exception    {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}

五、认证流程:

1、SpringSecurity体系中不包含JWT的认证,默认使用session认证,如果需要使用token认证那么需要自行定义token解析用的过滤器,进行token验证,并在配置类中关闭拦截器的session认证,将token拦截器添加到拦截器链中

2、自定义token验证过滤器

1)、通过request进行token解析

2)、若token不存在则表示当前用户未登录,直接放行(后续由其他拦截器进行拦截,在上述配置中有体现)

3)、若token存在则进行token解析,将解析的结果封装成LoginUser(用户登录的实体类含用户权限信息)进行返回,并存储到Redis中

4)、若用户已登录则将用户及其权限信息存入SecurityContextHolder中方便后续的拦截器调用

PS:SecurityContextHolder是SpringSecurity最基本的组件了,是用来存放SecurityContext的对象,默认是使用ThreadLocal实现的,这样就保证了本线程内所有的方法都可以获得SecurityContext对象

//解析token的相关代码
public LoginUser getLoginUser(HttpServletRequest request) {
    // 获取请求携带的令牌    String token = getToken(request);
    if (StringUtils.isNotEmpty(token)) {
        Claims claims = parseToken(token);
        // 解析对应的权限以及用户信息        
        String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
        String userKey = getTokenKey(uuid);
        LoginUser user = redisCache.getCacheObject(userKey);
        return user;
    }
    return null;
}
@Componentpublic
 class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired    
    private TokenService tokenService;

    @Override    
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        LoginUser loginUser = tokenService.getLoginUser(request);//从redis中获取登录用户的信息
        //若loginUser不为null则表示当前用户已登录 将用户和用户权限信息存入到SecurityContextHolder中,方便后续的取用
        if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
            tokenService.verifyToken(loginUser);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        chain.doFilter(request, response);
    }
}

2、若用户未登录,则进入登录接口(Jwt令牌的解析在登录确认前,在上述配置类中已配置)

1)、先清除Redis中的验证码数据,防止数据入侵

2)、验证验证码的正确性

3)、通过authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password))

方法验证账户密码,此方法底层会调用UserDetailsService接口实现类的 loadUserByUsername方法来验证用户名和密码,这个实现类和方法需要自定义。

4)、若上述验证正确,则会返回一个LoginUser对象并存入authentication对象的Principal属性中,再将Principal属性在登录服务中(SysLoginService)进行强转成LoginUser,并以此生成token返回给Controller层,由Controller进行封装返回给前端

5)、至此使用SpringSecurity进行用户的登录验证功能就完成,下次用户进行访问时会走Jwt令牌解析,进行权限的访问等相关验证

@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    SysUser user = userService.selectUserByUserName(username);
    if (StringUtils.isNull(user)) {
        log.info("登录用户:{} 不存在.", username);
        throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
    }
    else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
        log.info("登录用户:{} 已被删除.", username);
        throw new BaseException("对不起,您的账号:" + username + " 已被删除");
    }
    else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
        log.info("登录用户:{} 已被停用.", username);
        throw new BaseException("对不起,您的账号:" + username + " 已停用");
    }

    return createLoginUser(user);
}
@Componentpublic 
class SysLoginService {
    @Autowired    
    private TokenService tokenService;//token解析和生成工具类

    @Resource    
    private AuthenticationManager authenticationManager;//SpringSecurity中提供的认证管理器

    @Autowired    
    private RedisCache redisCache;//redis操作工具类

    /**     * 登录验证     *      * @param username 用户名     * @param password 密码     * @param code 验证码     * @param uuid 唯一标识     * @return 结果     */    
    public String login(String username, String password, String code, String uuid) {
        //删除redis中登录用的验证码
        String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
        String captcha = redisCache.getCacheObject(verifyKey);
        redisCache.deleteObject(verifyKey);
        //登录错误判断及日志存储
        if (captcha == null)
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
            throw new CaptchaExpireException();
        }
        if (!code.equalsIgnoreCase(captcha))
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
            throw new CaptchaException();
        }
        // 用户验证        
        Authentication authentication = null;
        try {
            /**             * !!!看我看我看我!!!            
             * 看我少走弯路,获取用户对象的时候,会去调用下面的这个方法查询用户对象            
              * UserDetailsServiceImpl.loadUserByUsername             */            
              // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername            
              System.out.println("username "+username+" -----password "+password);
            authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
        }catch (Exception e) {
            e.printStackTrace();
            if (e instanceof BadCredentialsException) {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
                throw new UserPasswordNotMatchException();
            }else {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
                throw new CustomException(e.getMessage());
            }
        }
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        // 生成token        
        return tokenService.createToken(loginUser);
    }
}

六、鉴权流程:

上述经过登录验证流程后,进入系统SpringSecurity鉴权流程,其中鉴权的核心是FilterSecurityInterceptor拦截器,搭配@PreAuthorize注解实现接口权限控制

鉴权流程:

在登录验证的过程中,会将用户的信息会存储在缓存和SecurityContextHolder中,在鉴权时即可拿出来,与Controller层中接口的方法上方的注解方法进行比对,若符合则放行,若不符则直接返回提示消息给前端

PS:关于前端模块的显隐也是由SpringSecurity和前端框架结合进行控制,核心jar时Thymeleaf

示例:

@PreAuthorize("@ss.hasPermi('clues:clue:false')")
@Log(title = "上传线索", businessType = BusinessType.UPDATE)
@PutMapping("/false/{id}")
public AjaxResult falseClue(@PathVariable String id, @RequestBody FalseClueDTO falseClueDTO){

其中‘clues:clue:false’代表当前接口访问所需的权限,@ss是自定义的一个鉴权方法,代码如下:

package com.huike.framework.web.service;

import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import com.huike.common.core.domain.entity.SysRole;
import com.huike.common.core.domain.model.LoginUser;
import com.huike.common.utils.ServletUtils;
import com.huike.common.utils.StringUtils;

/** * 自定义权限实现,ss取自SpringSecurity首字母 * @author wgl */@Service("ss")
public class PermissionService {
    /** 所有权限标识 */   
     private static final String ALL_PERMISSION = "*:*:*";

    /** 管理员角色权限标识 */   
     private static final String SUPER_ADMIN = "admin";

    private static final String ROLE_DELIMETER = ",";

    private static final String PERMISSION_DELIMETER = ",";

    @Autowired   
     private TokenService tokenService;

    /**     * 验证用户是否具备某权限     *      * @param permission 权限字符串     * @return 用户是否具备某权限     */    public boolean hasPermi(String permission) {
        if (StringUtils.isEmpty(permission)) {
            return false;
        }
        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) {
            return false;
        }
        return hasPermissions(loginUser.getPermissions(), permission);
    }

    /**     * 验证用户是否不具备某权限,与 hasPermi逻辑相反     *     
    * @param permission 权限字符串     * @return 用户是否不具备某权限     
    */   
     public boolean lacksPermi(String permission)
    {
        return hasPermi(permission) != true;
    }

    /**     * 验证用户是否具有以下任意一个权限     *    
     * @param permissions 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表    
      * @return 用户是否具有以下任意一个权限     */    
     public boolean hasAnyPermi(String permissions)
    {
        if (StringUtils.isEmpty(permissions))
        {
            return false;
        }
        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
        {
            return false;
        }
        Set<String> authorities = loginUser.getPermissions();
        for (String permission : permissions.split(PERMISSION_DELIMETER))
        {
            if (permission != null && hasPermissions(authorities, permission))
            {
                return true;
            }
        }
        return false;
    }

    /**     * 判断用户是否拥有某个角色     *      * @param role 角色字符串     * @return 用户是否具备某角色     */    
    public boolean hasRole(String role)
    {
        if (StringUtils.isEmpty(role))
        {
            return false;
        }
        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
        {
            return false;
        }
        for (SysRole sysRole : loginUser.getUser().getRoles())
        {
            String roleKey = sysRole.getRoleKey();
            if (SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role)))
            {
                return true;
            }
        }
        return false;
    }

    /**     * 验证用户是否不具备某角色,与 isRole逻辑相反。     *     * @param role 角色名称     * @return 用户是否不具备某角色     */    
    public boolean lacksRole(String role)
    {
        return hasRole(role) != true;
    }

    /**     * 验证用户是否具有以下任意一个角色     *     * @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表     * @return 用户是否具有以下任意一个角色     */    
    public boolean hasAnyRoles(String roles)
    {
        if (StringUtils.isEmpty(roles))
        {
            return false;
        }
        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
        {
            return false;
        }
        for (String role : roles.split(ROLE_DELIMETER))
        {
            if (hasRole(role))
            {
                return true;
            }
        }
        return false;
    }

    /**     * 判断是否包含权限     *      * @param permissions 权限列表     * @param permission 权限字符串     * @return 用户是否具备某权限     */    
    private boolean hasPermissions(Set<String> permissions, String permission)
    {
        return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值