springcloud微服务体系(一)— 基于security和jwt实现认证及鉴权服务

文章目录

  • 需求
  • 知识点讲解
    • 方案
    • SpringSecurity
  • 具体实现
    • 业务流程
    • 代码
    • 认证服务
    • 鉴权服务
    • 配置

需求

1、RESTfull风格的鉴权服务(路线相同的情况下根据请求方式鉴别访问权限)
2、包含用户、角色、权限
3、使用JWT最为token认证方式

知识点讲解

方案

传统的单体应用体系下,应用是一个整体,一般针对所有的请求都会进行权限校验。请求一般会通过一个权限的拦截器进行权限的校验,在登录时将用户信息缓存到 session 中,后续访问则从缓存中获取用户信息

但在微服务架构下,一个应用会被拆分成若干个微应用,每个微应用都需要对访问进行鉴权,每个微应用都需要明确当前访问用户以及其权限。尤其当访问来源不只是浏览器,还包括其他服务的调用时,单体应用架构下的鉴权方式就不是特别合适了。因此在设计架构中,要考虑外部应用接入的场景、用户与服务的鉴权、服务与服务的鉴权等多种鉴权场景。

目前主流的方案由四种

  1. 单点登录(SSO)

一次登入,多地使用。这种方案意味着每个面向用户的服务都必须与认证服务交互,进而产生大量琐碎的网络流量和重复的工作,当动辄数十个微应用时,这种方案的弊端会更加明显。

  1. 分布式 Session 方案

借助reids或其他共享存储中,将用户认证的信息存储在其中,通常使用用户会话作为 key 来实现的简单分布式哈希映射。当用户访问微服务时,用户数据可以从共享存储中获取。在某些场景下,这种方案很不错,用户登录状态是不透明的。同时也是一个高可用且可扩展的解决方案。这种方案的缺点在于共享存储需要一定保护机制,因此需要通过安全链接来访问,这时解决方案的实现就通常具有相当高的复杂性了。

  1. 客户端 Token 方案

令牌在客户端生成,由身份验证服务进行签名,并且必须包含足够的信息,以便可以在所有微服务中建立用户身份。令牌会附加到每个请求上,为微服务提供用户身份验证,这种解决方案的安全性相对较好,但身份验证注销是一个大问题,缓解这种情况的方法可以使用短期令牌和频繁检查认证服务等。对于客户端令牌的编码方案,Borsos 更喜欢使用 JSON Web Tokens(JWT),它足够简单且库支持程度也比较好。

  1. 客户端 Token 与 API 网关结合

这个方案意味着所有请求都通过网关,从而有效地隐藏了微服务。 在请求时,网关将原始用户令牌转换为内部会话 ID 令牌。在这种情况下,注销就不是问题,因为网关可以在注销时撤销用户的令牌。

本文就采用方案4,实现微服务体系中用户鉴权及认证服务。

Token的实现方案业界有多套成熟的方案,这其中最主流的是JWT 和 Oauth2.0 两种方式。
下面就基于JWT的方式具体实现。

SpringSecurity

AuthenticationManager, 用户认证的管理类,所有的认证请求(比如login)都会通过提交一个token给AuthenticationManager的authenticate()方法来实现。当然事情肯定不是它来做,具体校验动作会由AuthenticationManager将请求转发给具体的实现类来做。根据实现反馈的结果再调用具体的Handler来给用户以反馈。

AuthenticationProvider, 认证的具体实现类,一个provider是一种认证方式的实现,比如提交的用户名密码我是通过和DB中查出的user记录做比对实现的,那就有一个DaoProvider;如果我是通过CAS请求单点登录系统实现,那就有一个CASProvider。按照Spring一贯的作风,主流的认证方式它都已经提供了默认实现,比如DAO、LDAP、CAS、OAuth2等。
前面讲了AuthenticationManager只是一个代理接口,真正的认证就是由AuthenticationProvider来做的。一个AuthenticationManager可以包含多个Provider,每个provider通过实现一个support方法来表示自己支持那种Token的认证。AuthenticationManager默认的实现类是ProviderManager。

UserDetailService, 用户认证通过Provider来做,所以Provider需要拿到系统已经保存的认证信息,获取用户信息的接口spring-security抽象成UserDetailService。虽然叫Service,但是我更愿意把它认为是我们系统里经常有的UserDao。

AuthenticationToken, 所有提交给AuthenticationManager的认证请求都会被封装成一个Token的实现,比如最容易理解的UsernamePasswordAuthenticationToken。

SecurityContext,当用户通过认证之后,就会为这个用户生成一个唯一的SecurityContext,里面包含用户的认证信息Authentication。通过SecurityContext我们可以获取到用户的标识Principle和授权信息GrantedAuthrity。在系统的任何地方只要通过SecurityHolder.getSecruityContext()就可以获取到SecurityContext。在Shiro中通过SecurityUtils.getSubject()到达同样的目的。

具体实现

业务流程

  • 客户端调用登录接口,传入用户名密码。
  • 服务端请求身份认证中心,确认用户名密码正确。
  • 服务端创建JWT,返回给客户端。
  • 客户端拿到 JWT,进行存储(可以存储在缓存中,也可以存储在数据库中,如果是浏览器,可以存储在 Cookie中)在后续请求中,在 HTTP 请求头中加上 JWT。
  • 服务端校验 JWT,校验通过后,返回相关资源和数据。

代码

完整pom文件(项目结构为多模块)



    
        springcloud
        com.lhm
        1.0
    
    4.0.0

    security

    
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
        
            org.springframework.boot
            spring-boot-starter-security
        

        
            io.jsonwebtoken
            jjwt
            0.9.0
        

        
        
            com.baomidou
            mybatis-plus-boot-starter
            3.1.0
        

        
        
            p6spy
            p6spy
            3.8.1
        

        
            mysql
            mysql-connector-java
            runtime
        

        
        
            com.alibaba
            druid-spring-boot-starter
            1.1.9
        
        
        
            org.apache.commons
            commons-pool2
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
        
            com.alibaba
            fastjson
            1.2.38
        
        
        
            org.apache.commons
            commons-lang3
            3.4
        

        
        
            org.projectlombok
            lombok
            provided
        
    

认证服务

springcloud微服务体系(一)— 基于security和jwt实现认证及鉴权服务_第1张图片
在登入方面,本次使用了security默认提供的表单登陆方式,因此直接从实现UserDetailsService开始

package com.lhm.springcloud.security.service.impl;

import com.lhm.springcloud.security.constant.ResultCode;
import com.lhm.springcloud.security.exception.CommonException;
import com.lhm.springcloud.security.pojo.AuthUserDetails;
import com.lhm.springcloud.security.pojo.AuthUserPoJo;
import com.lhm.springcloud.security.service.IUsersService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

/**
 * @ClassName UserDetailsServiceImpl
 * @Description  实现security提供的 用户信息获取接口  并按照业务增加redis 登陆限制
 * @Author liuheming
 * @Date 2019/5/6 10:26
 * @Version 1.0
 **/
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
    //登入重试时间
    @Value("${security.loginAfterTime}")
    private Integer loginAfterTime;
    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private IUsersService iUsersService;

    /**
     * @Author liuheming
     * @Description 实现用户信息查询方法 让DaoAuthenticationProvider 获取到数据库获中用户数据
     * @Date 11:21 2019/5/6
     * @Param [username]
     * @return org.springframework.security.core.userdetails.UserDetails
     **/
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String flagKey = "loginFailFlag:"+username;
        String value = redisTemplate.opsForValue().get(flagKey);
        if(StringUtils.isNotBlank(value)){
            //超过限制次数
            throw new UsernameNotFoundException("登录错误次数超过限制,请"+loginAfterTime+"分钟后再试");
        }

        //查询用户信息
        AuthUserPoJo authUserPoJo=iUsersService.findAuthUserByUsername(username);
        if(null==authUserPoJo){
            throw new UsernameNotFoundException("当前用户不存在");
        }
        if(authUserPoJo.getRoleInfos()==null || authUserPoJo.getRoleInfos().isEmpty()){
            throw new UsernameNotFoundException("当前用户无角色");
        }
        return new AuthUserDetails(authUserPoJo);
    }
}

UserDetailsServiceImpl 最后返回一个拼装好的security用户对象,但为了实现自定义角色与权限管理需要对UserDetails进行重写。

package com.lhm.springcloud.security.pojo;

import com.lhm.springcloud.security.constant.UserConstant;
import com.lhm.springcloud.security.entity.PermissionInfo;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * @author Exrickx
 */

public class AuthUserDetails extends AuthUserPoJo implements UserDetails {

    private static final long serialVersionUID = 1L;

    public AuthUserDetails(AuthUserPoJo user) {
        if (user != null) {
            this.setUserName(user.getUserName());
            this.setPassWord(user.getPassWord());
            this.setStatus(user.getStatus());
            this.setRoleInfos(user.getRoleInfos());
            this.setPermissionInfos(user.getPermissionInfos());
        }
    }

    //将角色权限 放入GrantedAuthorit的自定义实现类MyGrantedAuthority中  为权限判定提供数据
    @Override
    public Collection getAuthorities() {

        List authorityList = new ArrayList();
        List permissions = this.getPermissionInfos();
        if (permissions != null) {
            for (PermissionInfo permission : permissions) {
                GrantedAuthority grantedAuthority = new MyGrantedAuthority(permission.getPath(), permission.getMethod());
                authorityList.add(grantedAuthority);
            }
        }
        return authorityList;
    }

    @Override
    public String getPassword() {
        return super.getPassWord();
    }

    @Override
    public String getUsername() {
        return super.getUserName();
    }


    /**
     * 账户是否过期
     *
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {

        return true;
    }

    /**
     * 是否禁用
     *
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {

        return true;
    }

    /**
     * 密码是否过期
     *
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {

        return true;
    }

    /**
     * 是否启用
     *
     * @return
     */
    @Override
    public boolean isEnabled() {
        return UserConstant.USER_STATUS_NORMAL.equals(this.getStatus()) ? true : false;
    }
}

然后DaoProvider会对比校验并执行相应的结果处理器

登入成功处理器

package com.lhm.springcloud.security.handler;

import com.lhm.springcloud.security.constant.ResultCode;
import com.lhm.springcloud.security.pojo.AuthUserDetails;
import com.lhm.springcloud.security.utils.ResUtil;
import com.lhm.springcloud.security.utils.ResponseUtil;
import com.lhm.springcloud.security.utils.TokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;

/**
 * @ClassName LoginSuccessHandlerFilter
 * @Description 登陆认证成功处理过滤器
 * @Author liuheming
 * @Date 2019/5/6 16:27
 * @Version 1.0
 **/
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
    @Autowired
    private TokenUtil tokenUtil;

    /**
     * @Author liuheming
     * @Description 用户认证成功后 生成token并返回
     * @Date 8:50 2019/5/7
     * @Param [request, response, authentication]
     * @return void
     **/
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

       AuthUserDetails authUserDetails=(AuthUserDetails)authentication.getPrincipal();//从内存中获取当前认证用户信息

        //创建token
        String accessToken = tokenUtil.createAccessJwtToken(authUserDetails);
        String refreshToken = tokenUtil.createRefreshToken(authUserDetails);

        HashMap map=new HashMap<>();
        map.put("accessToken",accessToken);
        map.put("refreshToken",refreshToken);

        ResponseUtil.out(response, ResUtil.getJsonStr(ResultCode.OK,"登录成功",map));
    }

}

登入失败处理器

package com.lhm.springcloud.security.handler;

import com.lhm.springcloud.security.constant.ResultCode;
import com.lhm.springcloud.security.utils.ResUtil;
import com.lhm.springcloud.security.utils.ResponseUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

/**
 * @ClassName LoginFailureHandler
 * @Description 登陆失败处理过滤器
 * @Author liuheming
 * @Date 2019/5/7 9:05
 * @Version 1.0
 **/
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
    //#限制用户登陆错误次数(次)
    @Value("${security.loginTimeLimit}")
    private Integer loginTimeLimit;
    //#错误超过次数后多少分钟后才能继续登录(分钟)
    @Value("${security.loginAfterTime}")
    private Integer loginAfterTime;

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * @Author liuheming
     * @Description 用户登陆失败处理类  记录用户登陆错误次数
     * @Date 9:12 2019/5/7
     * @Param [request, response, e]
     * @return void
     **/
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
            String username = request.getParameter("username");
            recordLoginTime(username);
            String key = "loginTimeLimit:" + username;
            String value = redisTemplate.opsForValue().get(key);
            if (StringUtils.isBlank(value)) {
                value = "0";
            }
            //获取已登录错误次数
            int loginFailTime = Integer.parseInt(value);
            int restLoginTime = loginTimeLimit - loginFailTime;
            ResponseUtil.out(response, ResUtil.getJsonStr(ResultCode.BAD_REQUEST, "用户名或密码错误"));
        } else if (e instanceof DisabledException) {
            ResponseUtil.out(response, ResUtil.getJsonStr(ResultCode.BAD_REQUEST, "账户被禁用,请联系管理员"));
        } else {
            ResponseUtil.out(response, ResUtil.getJsonStr(ResultCode.BAD_REQUEST, "登录失败"));
        }
    }
    /**
     * 判断用户登陆错误次数
     */
    public boolean recordLoginTime(String username) {

        String key = "loginTimeLimit:" + username;
        String flagKey = "loginFailFlag:" + username;
        String value = redisTemplate.opsForValue().get(key);
        if (StringUtils.isBlank(value)) {
            value = "0";
        }
        //获取已登录错误次数
        int loginFailTime = Integer.parseInt(value) + 1;
        redisTemplate.opsForValue().set(key, String.valueOf(loginFailTime), loginAfterTime, TimeUnit.MINUTES);
        if (loginFailTime >= loginTimeLimit) {

            redisTemplate.opsForValue().set(flagKey, "fail", loginAfterTime, TimeUnit.MINUTES);
            return false;
        }
        return true;
    }
}

在登入的过程中会对用户的请求间隔时间及失败次数做记录。

鉴权服务

鉴权的过程分成了两个大的步骤
第一对请求的路径、方法、头部信息进行判断,确认该请求是否需要鉴权
JWTAuthenticationFilter

package com.lhm.springcloud.security.filter;


import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.lhm.springcloud.security.constant.IgnoredUrlsProperties;
import com.lhm.springcloud.security.constant.ResultCode;
import com.lhm.springcloud.security.constant.SecurityConstant;
import com.lhm.springcloud.security.exception.CommonException;
import com.lhm.springcloud.security.pojo.MyGrantedAuthority;
import com.lhm.springcloud.security.utils.ResUtil;
import com.lhm.springcloud.security.utils.ResponseUtil;
import com.lhm.springcloud.security.utils.SpringUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;

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;

/**
 * JWT过滤器1
 */

public class JWTAuthenticationFilter extends BasicAuthenticationFilter {

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) {
        super(authenticationManager, authenticationEntryPoint);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {

        IgnoredUrlsProperties ignoredUrlsProperties= SpringUtil.getBean("ignoredUrlsProperties", IgnoredUrlsProperties.class);
        String Requesturl=request.getRequestURI();
        PathMatcher pathMatcher = new AntPathMatcher();
        if(null != ignoredUrlsProperties){
            for(String url:ignoredUrlsProperties.getUrls()){
                if(pathMatcher.match(url,Requesturl)){
                    chain.doFilter(request, response);
                    return;
                }
            }
        }

        //获取请求头
        String header = request.getHeader(SecurityConstant.HEADER);
        //如果请求头中不存在 或  格式不对  则进入下个过滤器
        if (StringUtils.isBlank(header) || !header.startsWith(SecurityConstant.TOKEN_SPLIT)) {
            chain.doFilter(request, response);
            return;
        }
        try {
            UsernamePasswordAuthenticationToken authentication = getAuthentication(request, response);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        } catch (Exception e) {
            ResponseUtil.out(response, ResUtil.getJsonStr(ResultCode.BAD_REQUEST, e.getMessage()));
            return;
        }

        chain.doFilter(request, response);
    }

    /**
     * @Author liuheming
     * @Description 对token进行解析认证
     * @Date 11:11 2019/5/7
     * @Param [request, response]
     * @return org.springframework.security.authentication.UsernamePasswordAuthenticationToken
     **/
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request, HttpServletResponse response) throws CommonException {

        String token = request.getHeader(SecurityConstant.HEADER);
        if (StringUtils.isNotBlank(token)) {
            // 解析token
            Claims claims = null;
            try {
                claims = Jwts.parser()
                        .setSigningKey(SecurityConstant.tokenSigningKey)
                        .parseClaimsJws(token.replace(SecurityConstant.TOKEN_SPLIT, ""))
                        .getBody();

                //获取用户名
                String username = claims.getSubject();

                //获取权限
                List authorities = new ArrayList();
                String authority = claims.get(SecurityConstant.AUTHORITIES).toString();

                if (StringUtils.isNotBlank(authority)) {
                    JSONArray list=JSONArray.parseArray(authority);
                    for (int i=0;i

第二判断当前请求token是否有权访问当前请求地址
MyFilterSecurityInterceptor

package com.lhm.springcloud.security.filter;

import com.lhm.springcloud.security.manager.MyAccessDecisionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import java.io.IOException;

/**
 * 权限管理过滤器2
 * 监控用户行为
 * @author Exrickx
 */

@Component
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

    @Autowired
    private FilterInvocationSecurityMetadataSource securityMetadataSource;

    @Autowired
    public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
        super.setAccessDecisionManager(myAccessDecisionManager);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        FilterInvocation fi = new FilterInvocation(request, response, chain);
        invoke(fi);
    }
    //fi里面有一个被拦截的url
    //里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
    //再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
    public void invoke(FilterInvocation fi) throws IOException, ServletException {

        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
    }

    @Override
    public void destroy() {

    }

    @Override
    public Class getSecureObjectClass() {
        return FilterInvocation.class;
    }

    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }
}

具体的处理会放到MySecurityMetadataSource中去判断,不过我这里做了个小优化,将处理权限的业务统一放到了MyAccessDecisionManager下,减少点性能开销

MySecurityMetadataSource

package com.lhm.springcloud.security.manager;

import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Collection;

/**
 * 权限资源管理器
 * 为权限决断器提供支持
 *
 * @author Exrickx
 */

@Component
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    /**
     * 此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
     * 因为每一次来了请求,都先要匹配一下权限表中的信息是不是包含此url,
     * 因此优化一下,对url直接拦截,不管请求的url 是什么都直接拦截,然后在MyAccessDecisionManager的decide 方法中做拦截还是放行的决策。
     * 所以此方法的返回值不能返回 null 此处随便返回一下。
     *
     * @param o
     * @return
     * @throws IllegalArgumentException
     */
    @Override
    public Collection getAttributes(Object o) throws IllegalArgumentException {

        Collection co = new ArrayList<>();
        co.add(new SecurityConfig("null"));
        return co;
    }

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

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

MyAccessDecisionManager

package com.lhm.springcloud.security.manager;

import com.lhm.springcloud.security.pojo.MyGrantedAuthority;
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.web.FilterInvocation;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.util.Collection;

/**
 * @ClassName MyAccessDecisionManager
 * @Description 权限最终判断器
 *  * 判断用户拥有的角色是否有资源访问权限
 * @Author liuheming
 * @Date 2019/5/7 10:44
 * @Version 1.0
 **/
@Service
public class MyAccessDecisionManager implements AccessDecisionManager {

    //decide 方法是判定是否拥有权限的决策方法
    @Override
    public void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
        String url, method;
        AntPathRequestMatcher matcher;
        for (GrantedAuthority ga : authentication.getAuthorities()) {
            if (ga instanceof MyGrantedAuthority) {
                MyGrantedAuthority urlGrantedAuthority = (MyGrantedAuthority) ga;
                url = urlGrantedAuthority.getPermissionUrl();
                method = urlGrantedAuthority.getMethod();
                matcher = new AntPathRequestMatcher(url);
                if (matcher.matches(request)) {
                    //当权限表权限的method为ALL时表示拥有此路径的所有请求方式权利。
                    if (method.equals(request.getMethod()) || "ALL".equals(method)) {
                        return;
                    }
                }
            }
            throw new AccessDeniedException("您没有访问权限");
        }
        throw new AccessDeniedException("鉴权出错");
    }



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

    @Override
    public boolean supports(Class clazz) {
        return true;
    }
}

decide()方法中的MyGrantedAuthority是我自定义的权限对象 因为原有的SimpleGrantedAuthority类只有一个属性,无法完成RESTfull风格的请求。
MyGrantedAuthority

package com.lhm.springcloud.security.pojo;

import org.springframework.security.core.GrantedAuthority;

/**
 * @ClassName MyGrantedAuthority
 * @Description TODO
 * @Author liuheming
 * @Date 2019/5/7 10:39
 * @Version 1.0
 **/
public class MyGrantedAuthority implements GrantedAuthority {
    private String url;
    private String method;

    public String getPermissionUrl() {
        return url;
    }

    public void setPermissionUrl(String permissionUrl) {
        this.url = permissionUrl;
    }

    public String getMethod() {
        return method;
    }

    public void setMethod(String method) {
        this.method = method;
    }

    public MyGrantedAuthority(String url, String method) {
        this.url = url;
        this.method = method;
    }

    @Override
    public String getAuthority() {
        return this.url + ";" + this.method;
    }
}

配置

最后将我们自定义的类全部注入到security提供的配置文件类中,具体的配置我都用注解表明了。

package com.lhm.springcloud.security.config;


import com.lhm.springcloud.security.constant.IgnoredUrlsProperties;
import com.lhm.springcloud.security.filter.JWTAuthenticationFilter;
import com.lhm.springcloud.security.filter.MyFilterSecurityInterceptor;
import com.lhm.springcloud.security.filter.WebSecurityCorsFilter;
import com.lhm.springcloud.security.handler.RestAccessDeniedHandler;
import com.lhm.springcloud.security.service.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

/*
 * Security 核心配置类
 * 开启控制权限至Controller
 * @author Exrickx
 * */


@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private IgnoredUrlsProperties ignoredUrlsProperties;

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Autowired
    private AuthenticationSuccessHandler successHandler;

    @Autowired
    private AuthenticationFailureHandler failHandler;

    @Autowired
    private RestAccessDeniedHandler accessDeniedHandler;

    @Autowired
    private MyFilterSecurityInterceptor myFilterSecurityInterceptor;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
        //密码加密使用 Spring Security 提供的BCryptPasswordEncoder.encode(user.getRawPassword().trim())
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = http
                .authorizeRequests();
        //除配置文件忽略路径其它所有请求都需经过认证和授权
        for(String url:ignoredUrlsProperties.getUrls()){
            registry.antMatchers(url).permitAll();
        }
        registry.antMatchers(HttpMethod.OPTIONS).permitAll()
                .and()
                //表单登录方式
                .formLogin()
                .loginPage("/login/needLogin")
                //登录需要经过的url请求
                .loginProcessingUrl("/api/v1/auth/login")
                .usernameParameter("username")
                .passwordParameter("password")
                .permitAll()
                //成功处理类
                .successHandler(successHandler)
                //失败
                .failureHandler(failHandler)
                .and()
                .logout()
                .permitAll()
                .and()
                .authorizeRequests()
                //任何请求
                .anyRequest()
                //需要身份认证
                .authenticated()
                .and()
                //关闭跨站请求防护
                .csrf().disable()
                //前后端分离采用JWT 不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //自定义权限拒绝处理类
                .exceptionHandling().accessDeniedHandler(accessDeniedHandler)
                .and()
                //添加自定义权限过滤器
                .addFilterBefore(new WebSecurityCorsFilter(), ChannelProcessingFilter.class)
                .addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class)
                //添加JWT过滤器 除/login其它请求都需经过此过滤器
                .addFilter(new JWTAuthenticationFilter(authenticationManager()));
    }


}

以上就是最核心的代码部分,完整代码已经贴出,有兴趣的同学可以结合代码学习一下。
git:https://github.com/liuheming/springcloudDemo.git

你可能感兴趣的:(java,springcloud)