SpringSecurity 微服务权限方案

1.什么是微服务

1.1微服务由来:

微服务最早由 Martin Fowler 与 James Lewis 于 2014 年共同提出,微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是 HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。

1.2微服务优势:

(1)微服务每个模块就相当于一个单独的项目,代码量明显减少,遇到问题也相对来说比较好解决。

(2)微服务每个模块都可以使用不同的存储方式(比如有的用 redis,有的用 mysql等),数据库也是单个模块对应自己的数据库。

(3)微服务每个模块都可以使用不同的开发技术,开发模式更灵活。

1.3微服务本质:

(1)微服务,关键其实不仅仅是微服务本身,而是系统要提供一套基础的架构,这种架构使得微服务可以独立的部署、运行、升级,不仅如此,这个系统架构还让微服务与微服务之间在结构上“松耦合”,而在功能上则表现为一个统一的整体。这种所谓的“统一的整体”表现出来的是统一风格的界面,统一的权限管理,统一的安全策略,统一的上线过程,统一的日志和审计方法,统一的调度方式,统一的访问入口等等。

(2)微服务的目的是有效的拆分应用,实现敏捷开发和部署。

2.微服务认证与授权实现思路

2.1认证授权过程分析:

(1)如果是基于 Session,那么 Spring-security 会对 cookie 里的 sessionid 进行解析,找到服务器存储的 session 信息,然后判断当前用户是否符合请求的要求。

(2)如果是 token,则是解析出 token,然后将当前请求加入到 Spring-security 管理的权限信息中去。

SpringSecurity 微服务权限方案

如果系统的模块众多,每个模块都需要进行授权与认证,所以我们选择基于 token 的形式进行授权与认证,用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限值,并以用户名为 key,权限列表为value 的形式存入 redis 缓存中,根据用户名相关信息生成 token 返回,浏览器将 token 记录到 cookie 中,每次调用 api 接口都默认将 token 携带到 header 请求头中,Spring-security 解析 header 头获取 token 信息,解析 token 获取当前用户名,根据用户名就可以从 redis 中获取权限列表,这样 Spring-security 就能够判断当前请求是否有权限访问。

2.2权限管理数据模型:

SpringSecurity 微服务权限方案

2.3JWT介绍:

1.访问令牌的类型:

SpringSecurity 微服务权限方案

2.JWT组成:

典型的,一个 JWT 看起来如下图:

SpringSecurity 微服务权限方案

该对象为一个很长的字符串,字符之间通过"."分隔符分为三个子串。

每一个子串表示了一个功能块,总共有以下三个部分:JWT 头、有效载荷和签名。

JWT 头

JWT 头部分是一个描述 JWT 元数据的 JSON 对象,通常如下所示。

{

    "alg": "HS256",
    "typ": "JWT"
}

在上面的代码中,alg 属性表示签名使用的算法,默认为 HMAC SHA256(写为 HS256);

typ 属性表示令牌的类型,JWT 令牌统一写为 JWT。最后,使用 Base64 URL 算法将上述

JSON 对象转换为字符串保存。

有效载荷

有效载荷部分,是 JWT 的主体内容部分,也是一个 JSON 对象,包含需要传递的数据。 JWT指定七个默认字段供选择。

iss:发行人

exp:到期时间

sub:主题

aud:用户

nbf:在此之前不可用

iat:发布时间

jti:JWT ID 用于标识该 JWT

除以上默认字段外,我们还可以自定义私有字段,如下例:

{

    "sub": "1234567890",
    "name": "Helen",
    "admin": true
}

请注意,默认情况下 JWT 是未加密的,任何人都可以解读其内容,因此不要构建隐私信息

字段,存放保密信息,以防止信息泄露。

JSON 对象也使用 Base64 URL 算法转换为字符串保存。

签名哈希

签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。

首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为 HMAC SHA256)根据以下公式生成签名。

HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(claims), secret)在计算出签名哈希后,JWT 头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用"."分隔,就构成整个 JWT 对象。

Base64URL 算法

如前所述,JWT 头和有效载荷序列化的算法都用到了 Base64URL。该算法和常见 Base64 算法类似,稍有差别。

作为令牌的 JWT 可以放在 URL 中(例如api.example/?token=xxx)。 Base64 中用的三个字符是"+","/“和”=",由于在 URL 中有特殊含义,因此Base64URL 中对他们做了替换:"=“去掉,”+“用”-“替换,”/“用”_"替换,这就是 Base64URL 算法。

2.4具体代码实现:

SpringSecurity 微服务权限方案

1 编写核心配置类

Spring Security 的核心配置就是继承 WebSecurityConfigurerAdapter 并注解@EnableWebSecurity 的配置。这个配置指明了用户名密码的处理方式、请求路径、登录登出控制等和安全相关的配置;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {

     //自定义查询数据库用户名密码和权限信息
     private UserDetailsService userDetailsService;
     //token 管理工具类(生成 token)
     private TokenManager tokenManager;
     //密码管理工具类
     private DefaultPasswordEncoder defaultPasswordEncoder;
     //redis 操作工具类
     private RedisTemplate redisTemplate;
     @Autowired
     public TokenWebSecurityConfig(UserDetailsService userDetailsService, 
     DefaultPasswordEncoder defaultPasswordEncoder,TokenManager tokenManager, RedisTemplate  redisTemplate) {

         this.userDetailsService = userDetailsService;
         this.defaultPasswordEncoder = defaultPasswordEncoder;
         this.tokenManager = tokenManager;
         this.redisTemplate = redisTemplate;
     }
     /**
     * 配置设置
     */
     //设置退出的地址和 token,redis 操作地址
     @Override
     protected void configure(HttpSecurity http) throws Exception {

         http.exceptionHandling()
         .authenticationEntryPoint(new UnauthorizedEntryPoint())
         .and().csrf().disable()
         .authorizeRequests()
         .anyRequest().authenticated()
         .and().logout().logoutUrl("/admin/acl/index/logout")
         .addLogoutHandler(new 
         TokenLogoutHandler(tokenManager,redisTemplate)).and()
         .addFilter(new TokenLoginFilter(authenticationManager(), 
         tokenManager, redisTemplate))
         .addFilter(new  TokenAuthenticationFilter(authenticationManager(), tokenManager,  redisTemplate)).httpBasic();
     }
     /**
     * 密码处理
     */
     @Override
     public void configure(AuthenticationManagerBuilder auth) throws Exception {

        auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
     }
     /**
     * 配置哪些请求不拦截
     */
     @Override
     public void configure(WebSecurity web) throws Exception {

         web.ignoring().antMatchers("/api/**" , "/swagger-ui.html/**);
     }
}

2.创建认证授权相关的工具类

SpringSecurity 微服务权限方案

(1)DefaultPasswordEncoder:密码处理的方法

@Component
public class DefaultPasswordEncoder implements PasswordEncoder {

     public DefaultPasswordEncoder() {

        this(-1);
     }
     /**
     * @param strength 
     * the log rounds to use, between 4 and 31
     */
     public DefaultPasswordEncoder(int strength) {

     }
     public String encode(CharSequence rawPassword) {

         return MD5.encrypt(rawPassword.toString());
     }
     public boolean matches(CharSequence rawPassword, String encodedPassword) {

         return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
     }
}

(2)TokenManager:token 操作的工具类

@Component
public class TokenManager {

     private long tokenExpiration = 24*60*60*1000;
     private String tokenSignKey = "123456";
     public String createToken(String username) {

         String token = Jwts.builder().setSubject(username)
         .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
         .signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
         return token;
     }
     public String getUserFromToken(String token) {

        String user = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
        return user;
     }
     public void removeToken(String token) {

        //jwttoken 无需删除,客户端扔掉即可。
     }
}

(3)TokenLogoutHandler:退出实现

public class TokenLogoutHandler implements LogoutHandler {

     private TokenManager tokenManager;
     private RedisTemplate redisTemplate;
     public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) {

         this.tokenManager = tokenManager;
         this.redisTemplate = redisTemplate;
     }
     @Override
     public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {

         String token = request.getHeader("token");
         if (token != null) {

             tokenManager.removeToken(token);
             //清空当前用户缓存中的权限数据
             String userName = tokenManager.getUserFromToken(token);
             redisTemplate.delete(userName);
         }
         ResponseUtil.out(response, R.ok());
    }
}

(4)UnauthorizedEntryPoint:未授权统一处理

public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {

     @Override
     public void commence(HttpServletRequest request, HttpServletResponse response,AuthenticationException authException) throws IOException, ServletException {

        ResponseUtil.out(response, R.error());
     }
}

2 创建认证授权实体类

SpringSecurity 微服务权限方案

(1) SecutityUser

@Data
@Slf4j
public class SecurityUser implements UserDetails {

     //当前登录用户
     private transient User currentUserInfo;
     //当前权限
     private List<String> permissionValueList;
     public SecurityUser() {

     }
     public SecurityUser(User user) {

         if (user != null) {

            this.currentUserInfo = user;
        }
     }
     @Override
     public Collection<? extends GrantedAuthority> getAuthorities() {

     Collection<GrantedAuthority> authorities = new ArrayList<>();
     for(String permissionValue : permissionValueList) {

         if(StringUtils.isEmpty(permissionValue)) continue;
         SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
         authorities.add(authority);
         }
        return authorities;
     }
     @Override
     public String getPassword() {

         return currentUserInfo.getPassword();
     }
     @Override
     public String getUsername() {

        return currentUserInfo.getUsername();
     }
     @Override
     public boolean isAccountNonExpired() {

        return true;
     }
     @Override
     public boolean isAccountNonLocked() {

         return true;
     }
     @Override
     public boolean isCredentialsNonExpired() {

         return true;
     }
     @Override
     public boolean isEnabled() {

        return true;
     }
}

(2)User

@Data
@ApiModel(description = "用户实体类")
public class User implements Serializable {

     private String username;
     private String password;
     private String nickName;
     private String salt;
     private String token;
}

3 创建认证和授权的 filter

SpringSecurity 微服务权限方案

(1)TokenLoginFilter:认证的 filter

public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

     private AuthenticationManager authenticationManager;
     private TokenManager tokenManager;
     private RedisTemplate redisTemplate;
     public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {

         this.authenticationManager = authenticationManager;
         this.tokenManager = tokenManager;
         this.redisTemplate = redisTemplate;
         this.setPostOnly(false);
         this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST"));
     }
     @Override
     public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException {

         try {

             User user = new ObjectMapper().readValue(req.getInputStream(), User.class);
             return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
         } catch (IOException e) {

             throw new RuntimeException(e);
         }
     }
     /**
     * 登录成功
     */
     @Override
     protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException {

         SecurityUser user = (SecurityUser) auth.getPrincipal();
         String token;
         tokenManager.createToken(user.getCurrentUserInfo().getUsername());

         redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), 
         user.getPermissionValueList());
         ResponseUtil.out(res, R.ok().data("token", token));
     }
     /**
     * 登录失败
     */
     @Override
     protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws 
     IOException, ServletException {

        ResponseUtil.out(response, R.error());
     }
}

(2)TokenAuthenticationFilter:授权 filter

public class TokenAuthenticationFilter extends BasicAuthenticationFilter {

     private TokenManager tokenManager;
     private RedisTemplate redisTemplate;
     public TokenAuthenticationFilter(AuthenticationManager authManager, TokenManager tokenManager,RedisTemplate redisTemplate) {

         super(authManager);
         this.tokenManager = tokenManager;
         this.redisTemplate = redisTemplate;
     }
     @Override
     protected void doFilterInternal(HttpServletRequest req,HttpServletResponse res, FilterChain chain) throws IOException, ServletException {

            logger.info("================="+req.getRequestURI());
             if(req.getRequestURI().indexOf("admin") == -1) {

                chain.doFilter(req, res);
                 return;
            }
            UsernamePasswordAuthenticationToken authentication = null;
            try {

                authentication = getAuthentication(req);
            } catch (Exception e) {

                ResponseUtil.out(res, R.error());
            }
             if (authentication != null) {

                SecurityContextHolder.getContext().setAuthentication(authentication);
             } else {

                ResponseUtil.out(res, R.error());
            }
            chain.doFilter(req, res);
        }
     private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {

     // token 置于 header 里
     String token = request.getHeader("token");
     if (token != null && !"".equals(token.trim())) {

         String userName = tokenManager.getUserFromToken(token);
         List<String> permissionValueList = (List<String>) 
         redisTemplate.opsForValue().get(userName);
         Collection<GrantedAuthority> authorities = new ArrayList<>();
        for(String permissionValue : permissionValueList) {

            if(StringUtils.isEmpty(permissionValue)) continue;
             SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
             authorities.add(authority);
         }
        if (!StringUtils.isEmpty(userName)) {

         return new UsernamePasswordAuthenticationToken(userName, token, authorities);
        }
        return null;
        }
        return null;
     }
}

3.完整流程图

SpringSecurity 微服务权限方案
SpringSecurity 微服务权限方案
SpringSecurity 微服务权限方案
SpringSecurity 微服务权限方案
SpringSecurity 微服务权限方案
SpringSecurity 微服务权限方案
SpringSecurity 微服务权限方案
SpringSecurity 微服务权限方案
SpringSecurity 微服务权限方案
SpringSecurity 微服务权限方案
SpringSecurity 微服务权限方案
SpringSecurity 微服务权限方案
SpringSecurity 微服务权限方案
SpringSecurity 微服务权限方案

4.SpringSecurity 原理总结

4.SpringSecurity 原理总结

1 SpringSecurity 的过滤器介绍

的设计模式,它有一条很长的过滤器链。现在对这条过滤器链的 15 个过滤器进行说明:

(1)
WebAsyncManagerIntegrationFilter:将 Security 上下文与SpringWeb 中用于处理异步请求映射的 WebAsyncManager 进行集成。

(2)
SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将SecurityContextHolder 中的信息清除,例如在 Session 中维护一个用户的安全信息就是这个过滤器处理的。

(3) HeaderWriterFilter:用于将头信息加入响应中。

(4) CsrfFilter:用于处理跨站请求伪造。

(5)LogoutFilter:用于处理退出登录。

(6)
UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和password,这两个值可以通过设置这个过滤器的usernameParameter 和passwordParameter 两个参数的值进行修改。

(7)
DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。

(8)BasicAuthenticationFilter:检测和处理 http basic 认证。

(9)RequestCacheAwareFilter:用来处理请求的缓存。

(10)
SecurityContextHolderAwareRequestFilter:主要是包装请求对象 request。

(11)
AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在Authentication 对象,如果不存在为其提供一个匿名 Authentication。

(12)SessionManagementFilter:管理 session 的过滤器

(13)
ExceptionTranslationFilter:处理 AccessDeniedException 和AuthenticationException 异常。

(14)FilterSecurityInterceptor:可以看做过滤器链的出口。

(15)
RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的 remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。

2 SpringSecurity 基本流程

Spring Security采取过滤链实现认证与授权,只有当前过滤器通过,才能进入下一个过滤器:

SpringSecurity 微服务权限方案

绿色部分是认证过滤器,需要我们自己配置,可以配置多个认证过滤器。认证过滤器可以使用 Spring Security 提供的认证过滤器,也可以自定义过滤器(例如:短信验证)。认证过滤器要在 configure(HttpSecurity http)方法中配置,没有配置不生效。下面会重点介绍以下三个过滤器:

UsernamePasswordAuthenticationFilter 过滤器:该过滤器会拦截前端提交的 POST 方式的登录表单请求,并进行身份认证。

ExceptionTranslationFilter 过滤器:该过滤器不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。

FilterSecurityInterceptor 过滤器:该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,并由
ExceptionTranslationFilter 过滤器进行捕获和处理。

</article>

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容