(第二篇)微服务架构下无状态权限认证系统设计(Spring Security + Oauth2 + JWT)-- 实现

一、参数配置

1)配置参数:application.yml
springcloud:
  security:
    oauth2:
      clients[0]:
        clientId: zuul-server
        clientSecret: '12345678'
        accessTokenValiditySeconds: 7200  //通行token有效时间(秒)
        refreshTokenValiditySeconds: 604800  //刷新token有效时间(秒)
      clients[1]:
        clientId: user-server
        clientSecret: '123456'
        accessTokenValiditySeconds: 7200
        refreshTokenValiditySeconds: 604800


2)单个客户端配置封装类:OAuth2ClientProperties.java
@Getter @Setter public class OAuth2ClientProperties { 
    private String clientId; //客户端id
    private String clientSecret; //客户端秘钥
    private Integer accessTokenValiditySeconds;  //通行token有效时长
    private Integer refreshTokenValiditySeconds; //刷新token有效时长
}

3)从配置文件获取配置封装到配置集合中:OAuth2Properties.java
@ConfigurationProperties(prefix = "springcloud.security.oauth2") //匹配前缀为springcloud.security.oauth2的参数
public class OAuth2Properties
{
    private OAuth2ClientProperties[] clients = {};//把参数放到数组中,此处也可以从数据库获取,本例子直接配置获取
    public OAuth2ClientProperties[] getClients() {
        return clients;
    }

    //OAuth2ClientProperties 单个配置实体封装
    public void setClients(OAuth2ClientProperties[] clients) {
        this.clients = clients;
    }
}

4)核心配置--扫描、注册自定义配置参数

@Configuration
@EnableConfigurationProperties(OAuth2Properties.class)
public class OAuth2CoreConfig
{
}
二、业务配置
1)认证授权参数配置
@Configuration
@EnableResourceServer
@EnableAuthorizationServer  //开启授权服务
public class OAuth2Config1 extends AuthorizationServerConfigurerAdapter
{
    private JsonParser objectMapper = JsonParserFactory.create();
    
    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Autowired
    private OAuth2Properties oauth2Properties;//注入配置类
    
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        //这句不知道是干啥的,大概是开启一个身份验证表单的功能吧,就是弹出一个输入账户、密码的框
        oauthServer.allowFormAuthenticationForClients();
    }
    
    /*配置客户端基本信息,循环配置数组,加载到配置服务运行内存,用于授权判断*/
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 客户端配置化
        InMemoryClientDetailsServiceBuilder build = clients.inMemory();
        if (ArrayUtils.isNotEmpty(oauth2Properties.getClients())) {
            // 验证模式:刷新token,密码、验证码模式,也可根据客户端自定义配置
            for (OAuth2ClientProperties config : oauth2Properties.getClients()) {
                build.withClient(config.getClientId()).secret(config.getClientSecret())
                    .accessTokenValiditySeconds(config.getAccessTokenValiditySeconds())   //通行token
                    .refreshTokenValiditySeconds(config.getRefreshTokenValiditySeconds()) // 刷新token
                    .authorizedGrantTypes("refresh_token", "password", "authorization_code")//验证模式
                    .scopes("platform");//作用域,可以在配置中按实际业务自定义各个客户端的作用域,写死并不可取
            }
        }
    }
    
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //JWT方式
        endpoints.tokenStore(tokenStore())//设置token存储方式,见方法实现
        .tokenEnhancer(jwtTokenEnhancer()) //设置token的加密方式,见方法实现
        .authenticationManager(authenticationManager);//开启密码类型验证的bean
    }
    
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtTokenEnhancer());
    }
    
    /*JWT token生成规则,此处对原来的转换器 JwtAccessTokenConverter 下的方法 enhance() 进行重写,
      来补充生成token中自定义的业务字段,如组织架构id、用户id等字段*/
    @Bean
    public JwtAccessTokenConverter jwtTokenEnhancer() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter() {

            /*重写token生成方法,补充组织架构、用户id内容*/
            @Override
            public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
                DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(accessToken);
                Map info = new LinkedHashMap(accessToken.getAdditionalInformation());//附加信息map
                String tokenId = result.getValue();
                if (!info.containsKey(TOKEN_ID)) {
                    info.put(TOKEN_ID, tokenId);
                }
                else {
                    tokenId = (String) info.get(TOKEN_ID);
                }

                //自定义信息设置到token负载中
                SysUser user = (SysUser)authentication.getPrincipal();//SysUser:系统的登录用户实体,根据实际业务设计
                info.put("orgId", user.getOrgId());//操作用户所在的组织架构id
                info.put("userId", user.getId());//操作用户的id
                try {
                    //用户名,因为是中文,为避免乱码,在设置时进行了编码转换,此处先解码
                    info.put("uname", URLEncoder.encode(user.getName(), "GBK"));
                } catch (UnsupportedEncodingException e1) {
                    e1.printStackTrace();
                }
                info.put("loginName",  user.getLoginName());//当前登录名
                info.put("dataAccess", user.getDataAccess());//当前用户的数据权限(数组)
                
                result.setAdditionalInformation(info);//把解析好的附加信息设置到结果集中
                result.setValue(encode(result, authentication));//对设置好的结果集编码
                OAuth2RefreshToken refreshToken = result.getRefreshToken();//获取结果中的刷新token
                if (refreshToken != null) {
               //下面这一段,我也看不大懂,不知道从哪拼凑来的,勉强能用吧[捂脸],大概意思就是,生成token,与附加信息合并返回
                    DefaultOAuth2AccessToken encodedRefreshToken = new DefaultOAuth2AccessToken(accessToken);
                    encodedRefreshToken.setValue(refreshToken.getValue());
                    encodedRefreshToken.setExpiration(null);
                    try {
                        Map claims = objectMapper.parseMap(JwtHelper.decode(refreshToken.getValue()).getClaims());
                        if (claims.containsKey(TOKEN_ID)) {
                            encodedRefreshToken.setValue(claims.get(TOKEN_ID).toString());
                        }
                    }
                    catch (IllegalArgumentException e) {
                    }
                    Map refreshTokenInfo = new LinkedHashMap(
                            accessToken.getAdditionalInformation());
                    refreshTokenInfo.put(TOKEN_ID, encodedRefreshToken.getValue());
                    refreshTokenInfo.put(ACCESS_TOKEN_ID, tokenId);
                    encodedRefreshToken.setAdditionalInformation(refreshTokenInfo);//
                    DefaultOAuth2RefreshToken token = new DefaultOAuth2RefreshToken(
                            encode(encodedRefreshToken, authentication));
                    if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
                        Date expiration = ((ExpiringOAuth2RefreshToken) refreshToken).getExpiration();
                        encodedRefreshToken.setExpiration(expiration);
                        token = new DefaultExpiringOAuth2RefreshToken(encode(encodedRefreshToken, authentication), expiration);
                    }
                    result.setRefreshToken(token);
                }
                return result;
            }
        };
        
        //根据私钥加锁,客户端需要拿匹配的公钥进行解锁,保证安全性,私钥使用Java Keytool 工具生成
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("fzp-jwt.jks"),
            "123456789".toCharArray());
        converter.setKeyPair(keyStoreKeyFactory.getKeyPair("fzp-jwt"));
        return converter;
    }
}

2)基于web的security基本配置

@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true)//方法级别的安全支持
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
    @Autowired
    UserService userService;
    
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    
    /**
     * 代码解析:
     * 1.在内存中创建一个认证用户信息
     * 2.该认证名为forezp,密码为123456,有USER的角色
     * 防护工作:
     * 1.应用的每一个请求都需要验证
     * 2.自动生成一个登录表单
     * 3.可以用username和password来进行验证
     * 4.可以注销
     * 5.阻止CSRF攻击
     * 6.Session Fixation保护
     * 7.安全Header集成......
     * 8.Servlet API方法集成:
     * HttpServletRequest#getRemoteUser()
     * HttpServletRequest.html#getUserPrincipal()
     * HttpServletRequest.html#getUserInRole(String)
     * HttpServletRequest.html#login(String,String)
     * HttpServletRequest.html#logout()
     */
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //集成JWT
       http.csrf().disable().exceptionHandling().accessDeniedPage("/401")
            .and()
            .authorizeRequests().anyRequest().fullyAuthenticated()
            .and()
            .httpBasic()
            .and().rememberMe().and()
            .formLogin().loginPage("/login").failureUrl("/login?error").permitAll() //登录页面用户任意访问
            .and()
            .logout().permitAll(); //注销行为任意访问
    }
    
    /**
     * 用户信息服务userService,具体实现见实体
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //验证使用自定义的userService
        auth.userDetailsService(userService);//.passwordEncoder(new BCryptPasswordEncoder());
    }
}

3)userService用户服务类实现

@Service
public class UserService implements UserDetailsService {
    private static final Logger log = LoggerFactory.getLogger(UserService.class);

    @Autowired
    UserDao userDao;//用户dao

    @Autowired
    RoleDao roleDao;//角色dao

    @Autowired
    PermissionDao permissionDao;//权限dao

    /**
     * 根据登录名获取用户实体信息
     */
    @Override    
    public SysUser loadUserByUsername(String loginName) throws UsernameNotFoundException {
    try {
        SysUser user = new SysUser();
        List grantedAuthorities = new ArrayList();
        SysUser query = new SysUser();
        query.setLoginName(loginName);
        user = (SysUser) this.userDao.templateOne(query);
        if (user != null) {
            Map paramMap = new HashMap();
            paramMap.put("userId", user.getId());
            List permissions = permissionDao.findByAdminUserId(paramMap);//接口调用权限
            List> dataAccess = permissionDao.findDataAccessByAdminUserId(paramMap);//数据权限
            List orgList = new ArrayList();
            if ((dataAccess != null) && (dataAccess.size() > 0)) {
                for (Map map : dataAccess) {
                    if (map.get("id") != null) {
                        orgList.add(map.get("id"));
                    }
                }
            }
            user.setDataAccess(orgList);//组织架构数据权限
            for (SysPermission permission : permissions) {
                if ((permission != null) && (permission.getName() != null)) {
                    SysRole role = new SysRole(permission.getName());
                    grantedAuthorities.add(role);
                }
            }
            user.setAuthorities(grantedAuthorities);//接口方法权限
            return user;
        }
        throw new UsernameNotFoundException("admin: " + loginName + " do not exist!");
    } catch (Exception e) {
        throw new RuntimeException("根据登录名获取用户实体出错!");
    }
}
 
  

以上,授权中心的配置核心部分基本完成

 

三、资源配置(在网关完成,由网关统一拦截请求,并对请求进行身份验证)

1)先附上两个异常处理类

//token验证异常封装类
public class AuthExceptionEntryPoint implements AuthenticationEntryPoint
{

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
        AuthenticationException authException) throws ServletException {
        Map map = new HashMap();
        Throwable cause = authException.getCause();
        if(cause instanceof InvalidTokenException) {
            map.put("code", RespCode.INVALID_TOKEN);//402
            map.put("msg", "无效的token");
        }else{
            map.put("code", RespCode.UN_LOGIN);//401
            map.put("msg", "访问此资源需要完全的身份验证");
        }
        map.put("data", authException.getMessage());
        map.put("success", false);
        map.put("path", request.getServletPath());
        map.put("timestamp", String.valueOf(System.currentTimeMillis()));
        response.setContentType("application/json");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        try {
            ObjectMapper mapper = new ObjectMapper();
            mapper.writeValue(response.getOutputStream(), map);
        } catch (Exception e) {
            throw new ServletException();
        }
    }
}
//权限不足异常封装类
@Component("customAccessDeniedHandler")
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
        AccessDeniedException accessDeniedException)
        throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        Map map = new HashMap();
        map.put("code", RespCode.UNAUTHORIZED);//401
        map.put("msg", "权限不足");
        map.put("data", accessDeniedException.getMessage());
        map.put("success", false);
        map.put("path", request.getServletPath());
        map.put("timestamp", String.valueOf(System.currentTimeMillis()));
        ObjectMapper mapper = new ObjectMapper();
        response.setContentType("application/json");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().write(mapper.writeValueAsString(map));
    }
}

2)JWT解密配置类

@Configuration
public class JwtConfig {
    @Bean
    @Qualifier("tokenStore")
    public TokenStore tokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
    
    /**
     * 
     * @Title: jwtAccessTokenConverter
     * @Description: JWT解密
     * @return
     * JwtAccessTokenConverter
     */
    @Bean
    protected JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        Resource resource = new ClassPathResource("public.cert");//拿到配置的公钥文件
        String publicKey;
        try {
            //生成公钥
            publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        converter.setVerifierKey(publicKey);//用公钥解密
        return converter;
    }
}

3)资源服务配置类

@Configuration
@EnableResourceServer
@RefreshScope
@EnableAspectJAutoProxy(proxyTargetClass=true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter{
    @Autowired
    TokenStore tokenStore;
    
    @Autowired
    private PermitConfig permitConfig;//此处配置那些不需要权限验证的服务接口
    
    @Autowired
    MyFilterSecurityInterceptor myFilterSecurityInterceptor;//自定义的方法放行判定类
 
    @Override
    public void configure(HttpSecurity http) throws Exception {
        ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry authenticated = http.csrf().disable().authorizeRequests()
        .antMatchers("/config/**").permitAll();
        String[] permitPaths = permitConfig.getPermitPaths();
        if(permitPaths != null){
           authenticated.antMatchers(permitPaths).permitAll();
        }
        authenticated.antMatchers("/**").authenticated();  
        http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);//添加自定义方法过滤器
    }
    
    @Override
    public void configure(ResourceServerSecurityConfigurer resource) throws Exception {
        //增加自定义的错误异常接收类,封装错误信息
        resource.tokenStore(tokenStore).authenticationEntryPoint(new AuthExceptionEntryPoint())
            .accessDeniedHandler(new CustomAccessDeniedHandler());
    }

}

 

4)自定义方法过滤器,定义方法通行的规则

@Service
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter
{
    @Autowired
    private MyInvocationSecurityMetadataSourceService 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);
    }

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

5)自定义权限加载--数据库权限配置获取,打破代码注解接口限制,可为每一个url配置访问权限

@Service
public class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource,InitializingBean{
    @Autowired
    private RedisApi redisApi;//Redis接口服务,把配置放进redis,避免频繁查询数据库

    @Autowired
    private PermissionDao permissionDao; //权限dao

    private static ConcurrentHashMap> map = new ConcurrentHashMap>();

    /**
     * 加载权限:从数据权限表获取系统所有权限,解析(系统的所有权限包括列表权限、操作权限均先配置在数据库中)
     * 问题:用户登录时获取token中存入权限信息,中途权限修改后,token中的权限并不会变:需要重新登陆获取新的token后才生效
     */
    public void loadResourceDefine() {
        List permissions = permissionDao.all();//获取所有配置权限
        Collection array;
        ConfigAttribute cfg;
        map.clear();
        //map = new HashMap<>();
        for (SysPermission permission : permissions) {
            array = new ArrayList<>();
            cfg = new SecurityConfig(permission.getName());
            // 此处只添加了权限的请求名称,其实还可以添加更多权限的信息,例如请求方法到ConfigAttribute的集合中去。
            //此处添加的信息将会作为MyAccessDecisionManager类的decide的第三个参数。
            array.add(cfg);
            // 用权限的getUrl() 作为map的key,用ConfigAttribute的集合作为 value,
            map.put(permission.getUrl(), array);
        }
        //Redis缓存服务中刷新标识归位(设置2分钟便于测试,修改权限时刷新该标识,可适当延长周期)
        redisApi.set(ProjectConstant.PERMISSION_UPDATE_FLAG, false, 2L, TimeUnit.MINUTES);
    }

    // 此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。
    // 如果不在权限表中则放行。
    @Override
    public Collection getAttributes(Object object) throws IllegalArgumentException {
        if (getRefreshFlag()) {
            //刷新标识开启时,重新加载配置列表(后台权限有修改时,该标识开启,说明权限信息有变更)
            loadResourceDefine();
        }
        // object 中包含用户请求的request 信息
        HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
        AntPathRequestMatcher matcher;
        String resUrl;
        for (Iterator iter = map.keySet().iterator(); iter.hasNext();) {
            resUrl = iter.next();
            matcher = new AntPathRequestMatcher(resUrl);
            if (matcher.matches(request)) { return map.get(resUrl); }
        }
        return null;
    }
    
    /**
     * 
     * @Title: getRefreshFlag
     * @Description: 检查权限map是否需要刷新--权限修改时,该标识生效,刷新map
     * @return
     * boolean
     * @throws
     */
    private boolean getRefreshFlag() {
        try {
            if (!redisApi.exists(ProjectConstant.PERMISSION_UPDATE_FLAG)) {
                //缓存不存在--首次,刷新
                return true;
            }else {
                if((Boolean) redisApi.get(ProjectConstant.PERMISSION_UPDATE_FLAG)) {
                    //权限被修改,刷新
                    return true;
                }else {
                    return false;
                }
            }
        } catch (Exception e) {
            //其他异常:刷新
            return true;
        }
    }

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

    @Override
    public boolean supports(Class clazz) {
        return true;
    }
    //初始化
   @Override
   public void afterPropertiesSet() throws Exception {
      loadResourceDefine();
   }
}

6)自定义权限验证类

@Service
public class MyAccessDecisionManager implements AccessDecisionManager
{

    // decide 方法是判定是否拥有权限的决策方法,
    // authentication 是UserService中循环添加到 GrantedAuthority 对象中的权限信息集合.
    // object 包含客户端发起的请求的requset信息,可转换为 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
    // configAttributes 为MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法返回的结果,此方法是为了判定用户请求的url
    // 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
    @Override
    public void decide(Authentication authentication, Object object, Collection configAttributes)
        throws AccessDeniedException, InsufficientAuthenticationException {
        if (null == configAttributes || configAttributes.size() <= 0) { 
            return;
        }
        ConfigAttribute c;
        String needRole;
        for (Iterator iter = configAttributes.iterator(); iter.hasNext();) {
            c = iter.next();
            needRole = c.getAttribute();
            // authentication 为在注释1 中循环添加到 GrantedAuthority对象中的权限信息集合
            for (GrantedAuthority ga : authentication.getAuthorities()) {
                if (needRole.trim().equals(ga.getAuthority())) { 
                    return; //有权限,放行
                }
            }
        }
        throw new AccessDeniedException("access denied");//无权限返回
    }

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

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

此处网关出的核心代码基本晒完了,可能有部分细节的遗漏。

以上方案为本人根据各项网站资料、书本资料及个人所感所悟所整合出来的权限管理方案,深知其中仍有许多不足,仅作个人总结及经验分享之用,请尊重个人劳动成果,勿用于商业用途,若以上内容有不足之处,欢迎广大道友指出!

 

 

 

你可能感兴趣的:(spring,cloud)