SpringCloud微服务间安全调用实现(SpringSecurity+Oauth2+Jwt)

一、流程

1、密码模式

客户端去认证中心申请访问凭证token,然后认证中心对于客户端请求来的帐号密码进行验证,如果验证通过,则颁发token,返回给客户端,客户端拿着 token 去各个微服务请求数据接口,一般这个 token 是放到 header 中的。当微服务接到请求后,先要拿着 token 去认证服务端检查 token 的合法性,如果合法,再根据用户所属的角色及具有的权限动态的返回数据

2、令牌模式

图片描述
图片描述

开始进行授权过程以前,博客园用户认证服务(三方应用)应用先要到QQ认证中心(授权服务器)上进行注册

  • 向授权服务器提供一个回调地址,作用是当QQ认证中心认证成功后,返回到具体博客园的哪个地址通知登录成功,如:https://auth.cnblogs.com/cc
  • 从授权服务器中获取ClientID(6666666)ClientSecret(8888888888)(自己独立保存),以便能够顺利完成如下授权过程。

第一步:第三方应用将用户(资源所有者)导向授权服务器的授权页面(即让用户在登录页面点击“企鹅”时),并向授权服务器提供 ClientID 及用户同意授权后的回调URI,这是第一次客户端页面转向。

第二步:授权服务器根据 ClientID 确认第三方应用的身份(博客园),用户在授权服务器中决定是否同意向该身份的应用进行授权,注意,用户认证的过程未定义在些步骤中,在些之前应该已经完成。
图片描述

第三步:如果用户同意授权,授权服务器将转向第三方应用在第1步调用中提供的回调URI(https://auth.cnblogs.com/cc),并附带上一个授权码获取令牌的地址作为参数,如:https://auth.cnblogs.com/cc?scode=1878103289012&token_url=https://auth.qq.com/xxxxxxxxxxx,这个地址会返回给浏览器(操作代理)发起第二个请求跳转。
第四步:浏览器(操作代理)跳转至回调地址(https://auth.cnblogs.com/cc?scode=1878103289012&token_url=https://auth.qq.com/xxxxxxxxxxx),用户认证中心(三方)收到授权码1878103289012,然后将授权码与自己内部存储ClientSecret(8888888888)一起作为参数,向QQ授权服务器提供的获取令牌的服务地址发起请求,如:https://auth.qq.com/xxxxxx?clientSecret=8888888&scode=1878103289012,换取令牌Token
第五步: QQ授权服务器核对授权码(1878103289012)和ClientSecret(8888888),确认无误后,向博客园用户认证中心(第三方)授予令牌Token,令牌可以是一个或者两个,其中必定要有访问令牌(Access Token),可选的是刷新令牌(Refresh Token)。访问令牌用于到资源服务器获取资源,有效期效短;刷新令牌用于在访问令牌失效后重新获取,有效期较长。
第六步: 博客服务器(资源服务器)根据访问令牌Token所允许的权限进行数据查询与交互

二、搭建微服务

一)认证服务器

1、yaml

基本不需要多的配置

点击查看代码
spring:
  redis:
    database: 4
    host: 192.168.52.10
    port: 6379
    password: Yifan123.
    jedis:
      pool:
        max-wait: 3600
        max-active: 1
        max-idle: 1
        min-idle: 1
    timeout: 3600

2、Java

1)认证服务器
点击查看代码
/**
 * 添加认证服务器配置,使用@EnableAuthorizationServer注解开启:
 * AuthorizationServer(认证服务器):用于认证用户的服务器,如果客户端认证通过,发放访问资源服务器的令牌。
 * 添加认证服务相关配置Oauth2ServerConfig,需要配置加载用户信息的服务UserServiceImpl及RSA的钥匙对KeyPair;
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserService userService;
    //Jwt内容增强器
    @Autowired
    private JwtTokenEnhancer jwtTokenEnhancer;
    @Autowired
    private DataSource dataSource;
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    /**
     * 配置Oauth2【客户端】(Client Details)请求访问【认证服务器】时身份验证信息
     * 确保只有经过授权的客户端能够向认证服务器进行有效的身份验证和令牌请求
     * 注意:秘钥(如ADMIN_CLIENT_ID、ADMIN_CLIENT_PASSWORD)是用于客户端与认证服务器之间的通信,与用户登录的密码是分开的概念。
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //方式一:设置客户端的配置从数据库中读取,存储在oauth_client_details表
        clients.withClientDetails(new JdbcClientDetailsService(dataSource));

        //方式二:这表示客户端详细信息将存储在内存中,而不是从数据库或其他外部源加载。
        /*clients.inMemory()
                //这里配置了一个客户端(client)的标识符,即client_id。在这个例子中,客户端标识符为"admin-app"。
                .withClient(Constant.ADMIN_CLIENT_ID)
                //这里设置了客户端的秘钥,用户客户端和认证服务器之间进行安全通信和身份验证
                .secret(passwordEncoder.encode(Constant.ADMIN_CLIENT_PASSWORD))
                //这里配置了客户端的权限范围(scopes)。"all"表示该客户端具有请求全部权限范围。
                .scopes("all")
                //这里配置了客户端支持的授权类型,"password"表示客户端可以使用用户名和密码进行身份验证,并获取访问令牌(access token),"refresh_token"表示客户端可以使用刷新令牌(refresh token)获取新的访问令牌。
                .authorizedGrantTypes("password", "refresh_token")
                //这里配置了访问令牌的有效期,单位为秒。在这个例子中,访问令牌的有效期为3600秒,即1小时。
                .accessTokenValiditySeconds(1000)
                //这里配置了刷新令牌的有效期(refresh token validity),单位为秒。在这个例子中,刷新令牌的有效期为86400秒,即24小时。
                .refreshTokenValiditySeconds(10000000);*/
    }

    /**
     * 配置了【授权服务器】的端点,包括身份验证管理器、用户信息服务、访问令牌转换器和令牌增强器。
     * 通过配置这些端点,可以实现对请求的身份验证、生成访问令牌和对访问令牌进行增强等功能。
     * 注意:在这段代码中没有包含对密钥对或密钥的配置,因此可能需要在其他地方进行相关配置,以便进行签名和验证操作。
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        List<TokenEnhancer> delegates = new ArrayList<>();
        //Jwt内容增强器
        delegates.add(jwtTokenEnhancer);
        //JWT令牌和OAuth2令牌之间的转换器
        delegates.add(accessTokenConverter());

        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        //将令牌增强器列表设置到TokenEnhancerChain对象中,以配置JWT的内容增强器。这将确保在生成JWT令牌时,会应用上述的令牌增强器。
        enhancerChain.setTokenEnhancers(delegates);

        endpoints.authenticationManager(authenticationManager)  //配置身份验证管理器(authenticationManager),用于处理身份验证请求。
                .userDetailsService(userService)                //配置加载用户信息的服务。
                .accessTokenConverter(accessTokenConverter())   //配置访问令牌转换器。
                .tokenEnhancer(enhancerChain);                  //配置令牌增强器,或指定 token 的存储方式。
    }

    /**
     * 生成JWT令牌转换器,并可使用密钥对对令牌进行签名和验证操作。这样配置的授权服务器可以生成和验证 JWT 令牌,以实现基于 JWT 的身份验证和授权功能。
     */
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        // JwtAccessTokenConverter是Spring Security OAuth2 提供的一个实现类,用于在 JWT(JSON Web Token)令牌和 OAuth2 令牌之间进行转换。
        // 它负责将 OAuth2 令牌转换为 JWT 格式的令牌,或将 JWT 格式的令牌转换为 OAuth2 令牌。
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setKeyPair(keyPair());
        return jwtAccessTokenConverter;
    }

    /**
     * 这个方法主要用于从一个位于 classpath 下的证书文件(jwt.jks)中获取一个密钥对。
     * KeyPair是Java 加密库提供的一个类,表示一个非对称密钥对,通常用于加密和签名操作。
     */
    @Bean
    public KeyPair keyPair() {
        //指定了位于 classpath 下的证书文件jwt.jks和证书密码
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
        //从证书中获取指定别名("jwt")和密码的密钥对
        return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
    }

    /**
     * 配置 token 节点的安全策略
     * 作用是允许客户端使用表单身份验证,以便授权服务器可以处理这种类型的客户端身份验证请求。这样,客户端可以使用表单提交其身份验证凭证,并与授权服务器进行交互。
     * 这个如果配置支持allowFormAuthenticationForClients的,且url中有client_id和client_secret的会走ClientCredentialsTokenEndpointFilter来保护
     * 如果没有支持allowFormAuthenticationForClients或者有支持但是url中没有client_id和client_secret的,走basic认证保护
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //允许客户端访问 OAuth2 授权接口,否则请求 token 会返回 401。
        security.allowFormAuthenticationForClients();
        //允许已授权用户访问 checkToken 接口
        security.checkTokenAccess("isAuthenticated()");
        //允许已授权用户获取 token 接口
        security.tokenKeyAccess("permitAll()");
    }
}
2)存储点(二选一)
a、JWT
点击查看代码
/**
 * Jwt内容增强器,比如往JWT中添加自定义信息的话,比如说登录用户的ID
 */
public class JwtTokenEnhancer implements TokenEnhancer {
    /**
     * Jwt内容增强器
     *
     * @param accessToken    原始的访问令牌
     * @param authentication 包含身份验证信息 对象
     * @return 返回增强后的访问令牌
     */
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        //从 authentication 对象中获取认证的主体(principal)。
        SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
        //把用户ID设置到JWT中
        Map<String, Object> info = new HashMap<>();
        info.put("id" , securityUser.getId());
        info.put("client_id" , securityUser.getClientId());
        //将 info 中的额外信息添加到访问令牌中。
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);

        return accessToken;
    }
}

/**
 * JWT内容增强器
 */
@Configuration
public class JwtTokenStoreConfig {
    /**
     * JWT内容增强器
     */
    @Bean
    public JwtTokenEnhancer jwtTokenEnhancer() {
        return new JwtTokenEnhancer();
    }
}
b、Redis
点击查看代码
@Configuration
public class RedisTokenStoreConfig {

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public TokenStore redisTokenStore (){
        return new RedisTokenStore(redisConnectionFactory);
    }
}
2)SpringSecurity配置
点击查看代码
/**
 * 添加SpringSecurity配置,允许认证相关路径的访问及表单登录, 实现HTTP定制化的安全配置。
 */
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * PasswordEncoder 是 Spring Security 提供的接口,用于对密码进行安全的哈希编码。
     * BCryptPasswordEncoder 是 Spring Security 提供的一种具体的实现,它使用 BCrypt 哈希算法来进行密码的编码和验证。
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 在 Spring Security 中,AuthenticationManager 是一个核心接口,用于处理身份验证相关的操作。
     * 通过获取 AuthenticationManager 实例,您可以在应用程序中进行用户身份验证的操作,如登录认证等。
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 定义HTTP请求的安全规则和访问控制策略。可以设置哪些请求需要身份验证、哪些请求需要特定的角色或权限、配置跨域请求、启用 CSRF 保护等。
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()           // 禁用 CSRF(跨站请求伪造)防护功能。
                .authorizeRequests()    //开始对请求进行授权配置。
                .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()   //允许所有的端点请求匹配器。
                .antMatchers("/oauth/token", "/oauth/check_token", "/rsa/publicKey", "/user/getAccessToken").permitAll()  //对指定的路径进行授权配置,允许所有用户访问。
                .anyRequest().authenticated();  //对其他所有请求进行身份验证。
    }
}
3)UserDetailsService
点击查看代码
/**
 * 设置用户验证服务。
 */
@Service
public class UserService implements UserDetailsService {
    @Autowired
    private UserCilents userCilent;
    @Autowired
    private HttpServletRequest request;
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // String password = passwordEncoder.encode("123456789");
        String clientId = request.getParameter("client_id");

        UserDTO userDTO = null;
        if (Constant.ADMIN_CLIENT_ID.equals(clientId)) {
            userDTO = userCilent.loadUserByUsername(username);
        } else {
            //userDto = memberService.loadUserByUsername(username);
        }

        if (userDTO == null) {
            throw new UsernameNotFoundException(Constant.USERNAME_PASSWORD_ERROR);
        }
        userDTO.setClientId(clientId);
        SecurityUser securityUser = new SecurityUser(userDTO);

        if (!securityUser.isEnabled()) {
            throw new DisabledException(Constant.ACCOUNT_DISABLED);
        } else if (!securityUser.isAccountNonLocked()) {
            throw new LockedException(Constant.ACCOUNT_LOCKED);
        } else if (!securityUser.isAccountNonExpired()) {
            throw new AccountExpiredException(Constant.ACCOUNT_EXPIRED);
        } else if (!securityUser.isCredentialsNonExpired()) {
            throw new CredentialsExpiredException(Constant.CREDENTIALS_EXPIRED);
        }
        return securityUser;
    }
}

二)sys服务器

1、UserDTO

点击查看代码
/**
 * @author 易樊
 * @since 2023-07-17
 */
@Data
@ApiModel(value = "User对象")
public class UserDTO implements Serializable {
    private static final long serialVersionUID = 1L;

    @ApiModelProperty("主键")
    private Long id;

    @ApiModelProperty("用户名")
    private String username;

    @ApiModelProperty("密码")
    private String password;

    @ApiModelProperty("认证标识:区分走admin的用户认证还是member的用户认证")
    private String clientId;

    @ApiModelProperty("是否启用")
    private Boolean isEnable;

    @ApiModelProperty("角色集合")
    private List<RoleDTO> roleDTOList;

    @ApiModelProperty("权限集合")
    private List<String> permissionList;
}

2、UserController

点击查看代码
@Slf4j
@RestController
@RequestMapping("/user")
@Api(value = "API", tags = {""})
public class UserController extends BaseController<User> {

    @Autowired
    private UserService userService;


    @ApiOperation("根据用户名获取通用用户信息")
    @RequestMapping(value = "/loadByUsername", method = RequestMethod.GET)
    @ResponseBody
    public UserDTO loadUserByUsername(@RequestParam String username) {
        UserDTO user = userService.loadUserByUsername(username);
        return user;
    }
}

3、UserService

点击查看代码
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleMapper roleMapper;
    @Autowired
    private PermissionMapper permissionMapper;

    @Override
    public UserDTO loadUserByUsername(String username) {
        UserDTO userDTO = null;
        User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, username));
        if (ObjectUtil.isNotEmpty(user)) {
            userDTO = BeanUtil.copyProperties(user, UserDTO.class);
            List<Role> roleList = roleMapper.getRoleByUser(user.getId());
            if (CollectionUtil.isNotEmpty(roleList)) {
                //加载用户角色
                List<RoleDTO> roleDTOList = DataUtil.toBeanList(roleList, RoleDTO.class);
                userDTO.setRoleDTOList(roleDTOList);

                //加载用户权限
                List<Map<String, String>> permissionList = permissionMapper.getPermissionByRole(roleList);
                System.out.println(JSON.toJSONString(permissionList));
                List<String> result = permissionList.stream().flatMap(map -> {
                    String[] typeArray = map.get("types").split(",");
                    return Arrays.stream(typeArray).map(type -> map.get("authority") + "_" + type);
                }).collect(Collectors.toList());
                userDTO.setPermissionList(result);
            }
        }
        return userDTO;
    }

}

三)资源服务器

1、Java

点击查看代码
/**
 * 配置了一个基于 JWT 的资源服务器,它使用 JWT 令牌来实现身份验证和授权
 */
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)//启用权限表达式
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Value("${security.oauth2.authorization.check-token-access}")
    private String checkTokenAccess;

    /**
     * 这个方法配置 HTTP 安全性,即定义哪些请求需要受保护,哪些请求允许公开访问
     * 允许所有用户访问 "/login" 路径,其他路径可能需要进行身份验证。
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests()
                .antMatchers("/api", "/api/**").permitAll()
                .anyRequest().authenticated();
    }

    /**
     * tokenServices 我们配置了一个 RemoteTokenServices 的实例,这是因为资源服务器和授权服务
     * 器是分开的,资源服务器和授权服务器是放在一起的,就不需要配置 RemoteTokenServices 了
     * RemoteTokenServices 中我们配置了 access_token 的校验地址、client_id、client_secret 这三个
     * 信息,当用户来资源服务器请求资源时,会携带上一个 access_token,通过这里的配置,就能够
     * 校验出 token 是否正确等。
     */
    @Bean
    RemoteTokenServices tokenServices() {
        RemoteTokenServices services = new RemoteTokenServices();
        services.setCheckTokenEndpointUrl(checkTokenAccess);
        services.setClientId(Constant.ADMIN_CLIENT_ID);
        services.setClientSecret(Constant.ADMIN_CLIENT_PASSWORD);
        return services;
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("res1").tokenServices(tokenServices());
    }
}
posted @ 2022-06-02 16:46  yifanSJ  阅读(1418)  评论(0编辑  收藏  举报