在管理后台的开发中经常需要对用户授权及权限控制,用户登录后,需要对用户拥有的角色来判断能够访问的资源

统一对登录后的token校验以及注解式的角色权限控制

首选简单创建5张表来存储用户、角色、资源表及用户-角色、角色-资源的关联表

创建一个适配器,来配置哪些接口需要鉴权,鉴权的方式

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class AuthServerConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private StringRedisTemplate redisTemplate;


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        LogoutConfigurer<HttpSecurity> httpSecurityLogoutConfigurer = http.cors().and().csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                .antMatchers(AuthWhiteList.AUTH_WHITE_LIST).permitAll()
                .anyRequest().authenticated()  // 所有请求需要身份认证
                .and()
                .addFilter(new JWTAuthenticationFilter(authenticationManager(), redisTemplate))
                .addFilter(new JWTLoginFilter(authenticationManager()))
                .exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint())// 自定义身份验证入口点
                .accessDeniedHandler(accessDeniedHandler()) // 自定义访问失败处理器
                .and()
                .formLogin()
                //.successHandler(authenticationSuccessHandler())// 认证成功处理器
                //.failureHandler(authenticationFailureHandler())// 认证失败处理器
                .and()
                .logout() // 默认注销行为为logout,可以通过下面的方式来修改
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login")// 设置注销成功后跳转页面,默认是跳转到登录页面;
                .permitAll();
    }

    /**
     * 指定加密方式
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 使用BCrypt加密密码
        return new MySecurityPasswordEncoder();
    }

    // 该方法是登录的时候会进入
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 使用自定义身份验证组件
        auth.authenticationProvider(new CustomAuthenticationProvider(userDetailsService, passwordEncoder()));
    }

    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint() {
        return new CustomAuthenticationEntryPoint();
    }

    @Bean
    public AuthenticationSuccessHandler authenticationSuccessHandler() {
        return new CustomAuthenticationSuccessHandler();
    }

    @Bean
    public AuthenticationFailureHandler authenticationFailureHandler() {
        return new CustomAuthenticationFailureHandler();
    }

    @Bean
    public AccessDeniedHandler accessDeniedHandler() {
        return new CustomAccessDeniedHandler();
    }

}

自定义认证过滤器 

/**
 * 自定义JWT认证过滤器
 * 该类继承自BasicAuthenticationFilter,在doFilterInternal方法中,
 * 从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。
 * 如果校验通过,就认为这是一个取得授权的合法请求
 */
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {


    private static final Logger logger = LoggerFactory.getLogger(JWTAuthenticationFilter.class);

    private StringRedisTemplate redisTemplate;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager, StringRedisTemplate redisTemplate) {
        super(authenticationManager);
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String requestURI = request.getRequestURI();
        String header = request.getHeader(ConstantKey.HEADER_KEY);
        if (StringUtils.isBlank(header) || !header.startsWith(ConstantKey.BEARER)) {
            chain.doFilter(request, response);
            return;
        }

        // 如果token不为空,并且是以指定票据开头
        if (StringUtils.isNotBlank(header) && header.startsWith(ConstantKey.BEARER)) {
            // 如果请求路径是放行路径,则直接跳过认证
            List<String> anonUrlList = Arrays.asList(AuthWhiteList.AUTH_WHITE_LIST);
            if (anonUrlList.contains(requestURI)) {
                chain.doFilter(request, response);
                return;
            }
        }

        UsernamePasswordAuthenticationToken authentication = getAuthentication(request, response);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(request, response);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        try {
            long start = System.currentTimeMillis();
            String token = request.getHeader(ConstantKey.HEADER_KEY);
            if (StringUtils.isBlank(token)) {
                throw new ServiceException("Token不能为空!");
            }
            // parse the token.
            Claims claims = JwtUtil.parseJWT(token.replace(ConstantKey.BEARER, ""));
            String userId = claims.getSubject();
            long end = System.currentTimeMillis();
            logger.info("执行时间: {}", (end - start) + " 毫秒");
            if (StringUtils.isNotBlank(userId)) {
                List<GrantedAuthority> authorities = new ArrayList<>();
                String roleStr = redisTemplate.opsForValue().get("user_role_" + userId);
                if (StringUtils.isNotBlank(roleStr)) {
                    List<String> authoritieStr = JSON.parseArray(roleStr, String.class);
                    for (String str : authoritieStr) {
                        authorities.add(new SimpleGrantedAuthority(str));
                    }
                }
                return new UsernamePasswordAuthenticationToken(userId, null, authorities);
            }
        } catch (ExpiredJwtException e) {
            // 异常捕获、发送到ExpiredJwtException
            request.setAttribute("expiredJwtException", e);
            // 将异常分发到ExpiredJwtException控制器
            request.getRequestDispatcher("/expiredJwtException").forward(request, response);
        } catch (UnsupportedJwtException e) {
            // 异常捕获、发送到UnsupportedJwtException
            request.setAttribute("unsupportedJwtException", e);
            // 将异常分发到UnsupportedJwtException控制器
            request.getRequestDispatcher("/unsupportedJwtException").forward(request, response);
        } catch (MalformedJwtException e) {
            // 异常捕获、发送到MalformedJwtException
            request.setAttribute("malformedJwtException", e);
            // 将异常分发到MalformedJwtException控制器
            request.getRequestDispatcher("/malformedJwtException").forward(request, response);
        } catch (SignatureException e) {
            // 异常捕获、发送到SignatureException
            request.setAttribute("signatureException", e);
            // 将异常分发到SignatureException控制器
            request.getRequestDispatcher("/signatureException").forward(request, response);
        } catch (IllegalArgumentException e) {
            // 异常捕获、发送到IllegalArgumentException
            request.setAttribute("illegalArgumentException", e);
            // 将异常分发到IllegalArgumentException控制器
            request.getRequestDispatcher("/illegalArgumentException").forward(request, response);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}

自定义加密方式

@Component
@Slf4j
public class MySecurityPasswordEncoder implements PasswordEncoder {

    @Autowired
    private JwtConfig jwtConfig;

    @Override
    public String encode(CharSequence charSequence) {
        String pwd = charSequence.toString();
        String encode = AESUtil.encrypt(pwd, jwtConfig.getAesKey());
        log.info("加密后:" + encode);
        return encode;
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        String pwd = charSequence.toString();
        // String md5Pwd = AESUtil.encrypt(pwd, jwtConfig.getAesKey());
        if (pwd.equals(s)) {
            return true;
        }
        log.info("--密码错误--");
        return false;
    }
}

登录后访问其他接口时的token校验

@Slf4j
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    public JWTLoginFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    /**
     * 尝试身份认证(接收并解析用户凭证)
     *
     * @param req
     * @param res
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException {
        try {
            UserEntity user = new ObjectMapper().readValue(req.getInputStream(), UserEntity.class);
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            user.getUserName(),
                            user.getPassword(),
                            new ArrayList<>())
            );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 认证成功(用户成功登录后,这个方法会被调用,我们在这个方法里生成token)
     *
     * @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 {
        // builder the token
        String token = null;
        try {
            Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
            // 定义存放角色集合的对象
            List roleList = new ArrayList<>();
            for (GrantedAuthority grantedAuthority : authorities) {
                roleList.add(grantedAuthority.getAuthority());
            }
            token = JwtUtil.createJWT(auth.getName() + "-" + roleList);
            // 生成token end

            // 登录成功后,返回token到header里面
            /*response.addHeader(ConstantKey.HEADER_KEY, ConstantKey.BEARER + token);*/

            // 登录成功后,返回token到body里面
            Map<String, Object> resultMap = new HashMap<>();
            resultMap.put(ConstantKey.HEADER_KEY, ConstantKey.BEARER + token);
            RestResponse result = RestResponse.success(resultMap);
            response.getWriter().write(JSON.toJSONString(result));

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

自定义用户登录时查询用户角色的方法

@Service
@Data
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity user = userMapper.selectOne(new LambdaQueryWrapper<UserEntity>().eq(UserEntity::getUserName, username));
        if (user == null) {
            throw new UsernameNotFoundException("用户名未找到!");
        }
        List<GrantedAuthority> authorities = new ArrayList<>();
        //查询用户角色
        List<String> roleList = userMapper.selectRoleByUserId(user.getId());
        if (!CollectionUtils.isEmpty(roleList)) {
            for (String str : roleList) {
                authorities.add(new SimpleGrantedAuthority(str));
            }
        }
        return new LoginUser(user, authorities);
    }

}

自定义身份验证,并生成令牌

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    private PasswordEncoder bCryptPasswordEncoder;

    public CustomAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.userDetailsService = userDetailsService;
        this.bCryptPasswordEncoder = passwordEncoder;
    }

    /**
     * 执行与以下合同相同的身份验证
     * {@link org.springframework.security.authentication.AuthenticationManager#authenticate(Authentication)}
     * 。
     *
     * @param authentication 身份验证请求对象。
     * @throws AuthenticationException 如果身份验证失败。
     * @返回包含凭证的经过完全认证的对象。 可能会回来
     * <code> null </ code>(如果<code> AuthenticationProvider </ code>无法支持)
     * 对传递的<code> Authentication </ code>对象的身份验证。 在这种情况下,
     * 支持所提供的下一个<code> AuthenticationProvider </ code>
     * 将尝试<code> Authentication </ code>类。
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 获取认证的用户名 & 密码
        String name = authentication.getName();
        String password = authentication.getCredentials().toString();
        // 认证逻辑
        LoginUser userDetails = (LoginUser) userDetailsService.loadUserByUsername(name);
        if (null == userDetails) {
            throw new UsernameNotFoundException("用户不存在!");
        }
        if (bCryptPasswordEncoder.matches(password, userDetails.getPassword())) {
            // 这里设置权限和角色
            List<GrantedAuthority> authorities = (List<GrantedAuthority>) userDetails.getAuthorities();
            // 生成令牌 这里令牌里面存入了:name,password,authorities, 当然你也可以放其他内容
            return new UsernamePasswordAuthenticationToken(userDetails.getUser().getId(), null, authorities);
        } else {
            throw new BadCredentialsException("密码错误!");
        }
    }

    /**
     * 是否可以提供输入类型的认证服务
     *
     * @param authentication
     * @return
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }

}

最后就是在登录方法中通过令牌来生成JWT的token放回给前端

public RestResponse<UserTokenVo> login(Map<String, String> param) {
        String userName = param.get("userName");
        String password = param.get("password");
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userName, AESUtil.encrypt(password, jwtConfig.getAesKey()));
        Authentication authentication = authenticationManager.authenticate(authenticationToken);
        if (Objects.isNull(authentication)) {
            return new RestResponse<>(ResponseCodeEnum.UNAUTHORIZED);
        }
        SecurityContextHolder.setContext(new SecurityContextImpl(authentication));
        //使用userid生成token
        String userId = String.valueOf(authentication.getPrincipal());
        //先在缓存中查找是否已登录,避免重复登录
        String oldToken = redisTemplate.opsForValue().get(userId);
        UserTokenVo token = new UserTokenVo();
        if (StringUtils.isNotBlank(oldToken)) {
            token = JSON.parseObject(oldToken, UserTokenVo.class);
        } else {
            String jwt = JwtUtil.createJWT(userId);
            String refreshJwt = JwtUtil.createRefreshJWT(userId);
            token.setRefreshToken(refreshJwt);
            token.setAccessToken(jwt);
            token.setUserName(userName);
            token.setExpire(String.valueOf(JwtUtil.JWT_TTL));
            redisTemplate.opsForValue().set("user_" + userId, JSON.toJSONString(token), JwtUtil.JWT_TTL, TimeUnit.MILLISECONDS);
        }
        //将用户的角色放入redis
        redisTemplate.opsForValue().set("user_role_" + userId, JSON.toJSONString(authentication.getAuthorities().stream().map(r -> r.getAuthority()).collect(Collectors.toList())));
        return RestResponse.success(token);
    }

特别注意:角色名要ROLE_开头

后面就是将登录返回的token放入请求头信息中

Authorization:Bearer {token}

在controller中也可以加入注解


@PreAuthorize("hasRole('ADMIN')")


来表示只有角色是admin的才能访问该接口