美文网首页大数据Java
SpringSecurity 微服务权限方案

SpringSecurity 微服务权限方案

作者: Java弟中弟 | 来源:发表于2021-11-21 13:25 被阅读0次

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>

相关文章

网友评论

    本文标题:SpringSecurity 微服务权限方案

    本文链接:https://www.haomeiwen.com/subject/aboptrtx.html