SpringBoot+SpringSecurity+jwt实现前后端分离的权限认证(不用security的登陆和注销)

涉及到的文件介绍

AjaxAccessDeniedHandler----用户权限不足时反给前端的数据
AjaxAuthenticationEntryPoint----用户没登陆时反给前端的数据
JwtAuthenticationTokenFilter----Jwt过滤器(第一个过滤器):获取用户token,查询用户信息拼装到security中,以便后续filter使用
JwtUtils----Jwt工具包
SpringSecurityConfig----Security核心配置文件
UrlAccessDecisionManager----自定义的比较逻辑;根据用户信息和权限去与当前访问的url需要的权限进行对比
UrlFilterInvocationSecurityMetadataSource----这个类是分析得出 用户访问的 url 需要哪些权限
其它关于权限模块的表和bean类我就不记了,用脚都能写出来。
下面是根据用户Id获取用户权限的sql:

SELECT a.permission FROM sys_resource a WHERE a.id in (
	SELECT resource_id FROM sys_role_resource WHERE role_id in (
		SELECT role_id FROM sys_user_role WHERE user_id = #{userId}
	)
)

下面是具体实现代码,无冗余代码,JwtAuthenticationTokenFilter中第40行的方法就是上面那条SQL

AjaxAccessDeniedHandler

import com.alibaba.fastjson.JSON;
import com.sonice1024.hongchen.common.response.ResponseCode;
import com.sonice1024.hongchen.common.response.ResponseResult;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 用户权限不足时反给前端的数据
 */
@Component
public class AjaxAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException {
        ResponseResult result = ResponseResult.build(ResponseCode.NO_AUTHORITY);
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setHeader("Content-Type", "application/json");
        httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
}

AjaxAuthenticationEntryPoint

import com.alibaba.fastjson.JSON;
import com.sonice1024.hongchen.common.response.ResponseCode;
import com.sonice1024.hongchen.common.response.ResponseResult;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 用户没登陆时反给前端的数据
 */
@Component
public class AjaxAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {

        ResponseResult result = ResponseResult.build(ResponseCode.TOKEN_FAIL);
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setHeader("Content-Type", "application/json");
        httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
}

JwtUtils

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang3.StringUtils;

import java.util.Date;

/**
 * Jwt工具包
 */
public class JwtUtils {

    private static final String SUBJECT = "***";

    private static final long EXPIRE = 1000*60*60*24*3;  //过期时间,毫秒,天
    // 秘钥
    private static final  String APPSECRET = "***";

    /**
     * 生成jwt的token串
     * @param username 用户名
     * @return String token字符串
     */
    public static String createJwtToken(String username){

        if(StringUtils.isBlank(username)){
            return null;
        }
        String token = Jwts.builder().setSubject(SUBJECT)
                .claim("username", username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis()+EXPIRE))
                .signWith(SignatureAlgorithm.HS512,APPSECRET)
                .compact();
        return token;
    }

    /**
     * 从token中获取用户名
     * @param token token字符串
     * @return Steing 用户名
     */
    public static String getUsername(String token){
        try {
            // 他自己会判断过期时间
            final Claims claims =  Jwts.parser().setSigningKey(APPSECRET).parseClaimsJws(token).getBody();
            if (claims != null) return (String) claims.get("username");
        } catch (Exception e) {
            return null;
        }
        return null;
    }
}

JwtAuthenticationTokenFilter

import com.sonice1024.hongchen.entity.SysUser;
import com.sonice1024.hongchen.service.SysUserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Jwt过滤器(第一个过滤器):获取用户token,查询用户信息拼装到security中,以便后续filter使用
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private SysUserService sysUserService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        // 获取token
        String tokenStr = request.getHeader("Authorization");
        if (StringUtils.isNotBlank(tokenStr)) {
            // 获取用户名
            String tokenObj = JwtUtils.getUsername(tokenStr);
            if (StringUtils.isNotBlank(tokenObj)) {

                // 查询用户信息和权限列表
                SysUser user = sysUserService.getAUserAndPermission(tokenObj);
                if (user != null) {
                    // 将权限列表给security,以便后续filter使用
                    List<SimpleGrantedAuthority> authorities = new ArrayList<>();
                    if (user.getPermissions() != null && user.getPermissions().size() > 0) {
                        authorities = user.getPermissions().stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
                    }
                    //设置当前上下文的认证信息
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(tokenObj, "", authorities);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }

        //调用下一个过滤器
        chain.doFilter(request, response);
    }
}

UrlFilterInvocationSecurityMetadataSource

import com.sonice1024.hongchen.entity.SysResource;
import com.sonice1024.hongchen.service.SysResourceService;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Collection;

/**
 * URL过滤器(第二个过滤器):
 * 1. 这个类是分析得出 用户访问的 url 需要哪些权限
 * 2. 核心的方法是第一个
 * 3. 第三个方法返回true表示支持支持这种方式即可
 */
@Component
public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    @Resource
    private SysResourceService sysResourceService;

    /**
     * 在用户发出请求时,根据请求的url查出该url需要哪些权限才能访问,并将所需权限给SecurityConfig
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        // 获取 请求 url 地址
        String requestUrl = ((FilterInvocation) o).getRequestUrl();
        // 得到所请求的 url 和 资源权限 的对应关系(这里可以用缓存处理)
        SysResource resource = sysResourceService.getPermissionByUrl(requestUrl);
        if (resource != null && resource.getVerification() == 1) {
            return SecurityConfig.createList(resource.getPermission());
        } else {
            // 如果都没有匹配上,我们返回默认值,这个值就像一个特殊的标识符,自定义,在UrlAccessDecisionManager中自定义规则即可
            return SecurityConfig.createList("ROLE_ANONYMOUS");
        }
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

UrlAccessDecisionManager

import org.springframework.http.HttpMethod;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.FilterInvocation;
import org.springframework.stereotype.Component;

import java.util.Collection;

/**
 * 根据用户信息和权限去与当前访问的url需要的权限进行对比
 */
@Component
public class UrlAccessDecisionManager implements AccessDecisionManager {

    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        //放行options请求
        FilterInvocation fi = (FilterInvocation) o;
        if (HttpMethod.OPTIONS.name().equals(fi.getRequest().getMethod())) {
            return;
        }
        // collection是url过滤器给过来的权限列表,判断url是否需要拦截
        for (ConfigAttribute attribute : collection) {
            if (!"ROLE_ANONYMOUS".equals(attribute.toString())) {   // 需要拦截的
                // 看是否有用户信息
                Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
                if (principal == null) throw new AccessDeniedException("expire");

                // 看权限是否足够
                Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
                for (GrantedAuthority authority : authorities) {
                    // authority.getAuthority()是jwt过滤器给过来的权限
                    // attribute.getAttribute()是url过滤器给过来的权限
                    if (authority.getAuthority().equals(attribute.getAttribute())) {
                        return;
                    }
                }
                throw new AccessDeniedException("not allow");
            }
        }
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

SpringSecurityConfig

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    AjaxAuthenticationEntryPoint authenticationEntryPoint;  //  未登陆时返回 JSON 格式的数据给前端(否则为 html)

    @Autowired
    AjaxAccessDeniedHandler accessDeniedHandler;    // 无权访问返回的 JSON 格式数据给前端(否则为 403 html 页面)

    @Autowired
    private JwtAuthenticationTokenFilter jwtRequestFilter;  // Jwt拦截器,获取用户的数据和用户拥有的权限

    @Autowired
    UrlAccessDecisionManager urlAccessDecisionManager;  // 自定义的权限比较器

    @Autowired
    UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource;    // 获取访问URL需要的权限

    /**
     * 配置说明:
     * 1。 因为整个系统是前后端分离的,不需要Security自带的登陆和退出登陆功能,所以不需要配置.formLogin()和.logout();
     * 如果我们使用session的方式记录用户登陆状态就需要在“登陆接口”将session保存到内存中,后面的授权Security会自己去拿session;
     * 如果用token方式,在“登陆接口”我们需要生成token给客户端,客户端在登陆后的其它请求中需要将token放在header中传入接口;
     * 默认是使用session保存用户登陆的状态的,我们需要改为使用token的模式:
     * 1. 用户访问接口,首先进入JwtAuthenticationTokenFilter,我们需要在这里面构建用户信息,包括用户有哪些权限
     * 2. 经过JwtAuthenticationTokenFilter,得到用户有哪些权限后,进入UrlFilterInvocationSecurityMetadataSource获取用户
     * 访问的url是否需要拦截
     * 3. 所有的数据都有了,在UrlAccessDecisionManager中自定义比较即可
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()//不使用防跨站攻击

                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//不使用session

                .and().authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // OPTIONS放行
                .anyRequest().authenticated()   // 其它的全部自定义验证
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setAccessDecisionManager(urlAccessDecisionManager);
                        o.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource);
                        return o;
                    }
                });

        http.headers().cacheControl();//http的cache控制,如下这句代码会禁用cache
        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);//添加JWT身份认证的filter
        //添加自定义未授权的处理器
        http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
        //添加自定义未登录的处理器
        http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
    }

}

有兴趣交流的可以关注公众号或加我的QQ群:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值