在微服务大行其道的今天,出门面试不会整两句springboot、springcloud都不好意思去面试。在java领域springcloud已经是微服务开发的事实标准了,使用springcloud+springboot开发微服务也很简单,但是在在开发过程中微服务的权限验证是个问题,怎么保证各微服务的安全,微服务的权限验证普遍用的都是osuth2.0的验证,但是在oauth2.0种验证方式也有不同:
1、客户端通过oauth服务器直接获取token,客户端携带token访问微服务、微服务链接oauth服务器验证token:
2、客户端通过oauth服务器获取jwt,客户端携带jwt访问微服务,微服务自己解析jwt验证权限
第一种使用简单但是有个致命的问题,所有的微服务都要访问ozuth服务器,势必会导致oauth服务器压力过大、所以本文基于第二种配置集成oauth,项目源码github传送https://github.com/qxxg/platform,感觉可以请帮忙点个star
项目模块结构:
项目创建不会的可以自行百度,直接介绍oauth2模块
创建platform-oauth项目添加oauth依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
由于oauth验证会访问数据库还要添加mybatis依赖:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
配置项目文件(这里用的时mysql8.0版本,8.0以下版本的自行修改数据库链接)
使用mybatis逆向工程生成数据库访问dao(不会使用逆向工程的可以自行百度或者手写,这里dao访问很简单)
接下来时oauth相关类,可以照搬。具体注意以下两个类:
SmsAuthenticator -------------》短信登录验证器
UsernamePasswordAuthenticator --》账号密码验证器
这两个类负责数据库的查询和验证。
1、IntegrationAuthenticationContext:
/**
* @Description: 集成认证上下文
*/
public class IntegrationAuthenticationContext {
private static ThreadLocal<IntegrationAuthenticationEntity> holder = new ThreadLocal<>();
public static void set(IntegrationAuthenticationEntity entity){
holder.set(entity);
}
public static IntegrationAuthenticationEntity get(){
return holder.get();
}
public static void clear(){
holder.remove();
}
}
2、IntegrationAuthenticationEntity:
/**
* @Description: 集成认证实体
*/
@Data
public class IntegrationAuthenticationEntity {
private String authType;//请求登录认证类型
private Map<String,String[]> authParameters;//请求登录认证参数集合
public String getAuthParameter(String paramter){
String[] values = this.authParameters.get(paramter);
if(values != null && values.length > 0){
return values[0];
}
return null;
}
}
3、IntegrationAuthenticationFilter:
/**
* @Description: 集成认证拦截器
*/
@Component
public class IntegrationAuthenticationFilter extends GenericFilterBean implements ApplicationContextAware {
private static final String AUTH_TYPE_PARM_NAME = "auth_type";//登录类型参数名
private static final String OAUTH_TOKEN_URL = "/oauth/token";//需要拦截的路由
private RequestMatcher requestMatcher;
private ApplicationContext applicationContext;
private Collection<IntegrationAuthenticator> authenticators;
public IntegrationAuthenticationFilter() {
this.requestMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(OAUTH_TOKEN_URL, "GET"),
new AntPathRequestMatcher(OAUTH_TOKEN_URL, "POST")
);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
if (requestMatcher.matches(request)){
RequestParameterWrapper requestParameterWrapper = new RequestParameterWrapper(request);
if (requestParameterWrapper.getParameter("password") == null){
requestParameterWrapper.addParameter("password","");
}
IntegrationAuthenticationEntity entity = new IntegrationAuthenticationEntity();
entity.setAuthType(requestParameterWrapper.getParameter(AUTH_TYPE_PARM_NAME));
entity.setAuthParameters(requestParameterWrapper.getParameterMap());
IntegrationAuthenticationContext.set(entity);
try {
this.prepare(entity);
filterChain.doFilter(requestParameterWrapper,servletResponse);
this.complete(entity);
} finally {
IntegrationAuthenticationContext.clear();
}
}
else {
filterChain.doFilter(servletRequest,servletResponse);
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* 认证前回调
* @param entity 集成认证实体
*/
private void prepare(IntegrationAuthenticationEntity entity) {
if (entity != null){
synchronized (this){
Map<String, IntegrationAuthenticator> map = applicationContext.getBeansOfType(IntegrationAuthenticator.class);
if (map != null){
this.authenticators = map.values();
}
}
}
if (this.authenticators == null){
this.authenticators = new ArrayList<>();
}
for (IntegrationAuthenticator authenticator : this.authenticators){
if (authenticator.support(entity)){
authenticator.prepare(entity);
}
}
}
/**
* 认证结束后回调
* @param entity 集成认证实体
*/
private void complete(IntegrationAuthenticationEntity entity) {
for (IntegrationAuthenticator authenticator: authenticators) {
if(authenticator.support(entity)){
authenticator.complete(entity);
}
}
}
/**
* 用途:在拦截时给Request添加参数
* Cloud OAuth2 密码模式需要判断Request是否存在password参数,
* 如果不存在会抛异常结束认证
* 所以在调用doFilter方法前添加password参数
*/
class RequestParameterWrapper extends HttpServletRequestWrapper {
private Map<String, String[]> params = new HashMap<String, String[]>();
public RequestParameterWrapper(HttpServletRequest request) {
super(request);
this.params.putAll(request.getParameterMap());
}
public RequestParameterWrapper(HttpServletRequest request, Map<String, Object> extraParams) {
this(request);
addParameters(extraParams);
}
public void addParameters(Map<String, Object> extraParams) {
for (Map.Entry<String, Object> entry : extraParams.entrySet()) {
addParameter(entry.getKey(), entry.getValue());
}
}
@Override
public String getParameter(String name) {
String[]values = params.get(name);
if(values == null || values.length == 0) {
return null;
}
return values[0];
}
@Override
public String[] getParameterValues(String name) {
return params.get(name);
}
@Override
public Map<String, String[]> getParameterMap() {
return params;
}
public void addParameter(String name, Object value) {
if (value != null) {
if (value instanceof String[]) {
params.put(name, (String[]) value);
} else if (value instanceof String) {
params.put(name, new String[]{(String) value});
} else {
params.put(name, new String[]{String.valueOf(value)});
}
}
}
}
}
4、IntegrationUserDetailsService:
/**
* @Description: 集成认证-用户细节服务
*/
@Service
public class IntegrationUserDetailsService implements UserDetailsService {
private List<IntegrationAuthenticator> authenticators;
@Autowired(required = false)
public void setIntegrationAuthenticators(List<IntegrationAuthenticator> authenticators) {
this.authenticators = authenticators;
}
@Override
public UserDetails loadUserByUsername(String str) throws UsernameNotFoundException {
IntegrationAuthenticationEntity entity = IntegrationAuthenticationContext.get();
if (entity == null){
entity = new IntegrationAuthenticationEntity();
}
UserPojo pojo = this.authenticate(entity);
if (pojo == null){
throw new OAuth2Exception("用户名或密码错误");
}
User user = new User(pojo.getName(),pojo.getPwd(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROOT_USER"));
return user;
}
private UserPojo authenticate(IntegrationAuthenticationEntity entity) {
if (this.authenticators != null) {
for (IntegrationAuthenticator authenticator : authenticators) {
if (authenticator.support(entity)) {
return authenticator.authenticate(entity);
}
}
}
return null;
}
}
5、AbstractPreparableIntegrationAuthenticator:
/**
* @Description: 集成认证-认证器抽象类
*/
public abstract class AbstractPreparableIntegrationAuthenticator implements IntegrationAuthenticator {
@Override
public void prepare(IntegrationAuthenticationEntity entity) {
}
@Override
public void complete(IntegrationAuthenticationEntity entity) {
}
}
6、IntegrationAuthenticator:
/**
* @Description: 集成认证-认证器接口
*/
public interface IntegrationAuthenticator {
/**
* 处理集成认证
* @param entity 集成认证实体
* @return 用户表实体
*/
UserPojo authenticate(IntegrationAuthenticationEntity entity);
/**
* 预处理
* @param entity 集成认证实体
*/
void prepare(IntegrationAuthenticationEntity entity);
/**
* 判断是否支持集成认证类型
* @param entity 集成认证实体
*/
boolean support(IntegrationAuthenticationEntity entity);
/**
* 认证结束后执行
* @param entity 集成认证实体
*/
void complete(IntegrationAuthenticationEntity entity);
}
7、SmsAuthenticator:
/**
* @Description: 短信认证器
*/
@Component
public class SmsAuthenticator extends AbstractPreparableIntegrationAuthenticator {
private final static String AUTH_TYPE = "sms";
@Autowired
private UserMapper mapper;
@Override
public UserPojo authenticate(IntegrationAuthenticationEntity entity) {
String mobile = entity.getAuthParameter("mobile");
if(StringUtils.isEmpty(mobile)){
throw new OAuth2Exception("手机号不能为空");
}
String code = entity.getAuthParameter("code");
//测试项目,所以将验证码顶死为:1234
if(! "1234".equals(code)){
throw new OAuth2Exception("验证码错误或已过期");
}
return mapper.findByMobile(mobile);
}
@Override
public boolean support(IntegrationAuthenticationEntity entity) {
return AUTH_TYPE.equals(entity.getAuthType());
}
}
8、UsernamePasswordAuthenticator:
/**
* @Description: 普通认证器(用户名+密码)
*/
@Component
@Primary
public class UsernamePasswordAuthenticator extends AbstractPreparableIntegrationAuthenticator {
@Autowired
private UserMapper mapper;
@Override
public UserPojo authenticate(IntegrationAuthenticationEntity entity) {
String name = entity.getAuthParameter("username");
String pwd = entity.getAuthParameter("password");
if(name == null || pwd == null){
throw new OAuth2Exception("用户名或密码不能为空");
}
UserPojo pojo = mapper.findByName(name);
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
if(encoder != null && encoder.matches(pwd,pojo.getPwd())){
return pojo;
}
return null;
}
@Override
public boolean support(IntegrationAuthenticationEntity entity) {
return StringUtils.isEmpty(entity.getAuthType());
}
}
接下来配置oauth授权服务配置,这里使用的时密钥的方式生成jwt,在微服务端使用公钥解析jwt:密钥生成命令
keytool -genkeypair -alias ltd-jwt -validity 3650 -keyalg RSA -dname "CN=jwt,OU=jwt,L=zurich,C=CH" -keypass ltd123 -keystore ltd-jwt.jks -storepass ltd123密钥生成需要使用到ssl现在自己电脑安装ssl如果只是测试下oauth在github项目源码种提供了一个key文件可以直接使用
1、AuthorizationServerConfigurer:
/**
* @Description: 授权服务器配置
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private IntegrationUserDetailsService integrationUserDetailsService;
//这里true,使全局密码结果为true,因为有些登录类型不需要验证密码,比如验证码登录,第三方系统登录等等,所以需要认证密码的要单独认证
@Bean
public PasswordEncoder passwordEncoder(){
return new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return "";
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return true;
}
};
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore())
.accessTokenConverter(jwtAccessTokenConverter())
.authenticationManager(authenticationManager )
.userDetailsService(integrationUserDetailsService);
super.configure(endpoints);
}
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* keytool -genkeypair -alias ltd-jwt -validity 3650 -keyalg RSA -dname "CN=jwt,OU=jwt,L=zurich,C=CH" -keypass ltd123 -keystore ltd-jwt.jks -storepass ltd123
* @return
*/
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
ClassPathResource resource = new ClassPathResource("ltd-jwt.jks");
KeyStoreKeyFactory ksf = new KeyStoreKeyFactory(resource,"ltd123".toCharArray());
KeyPair kp = ksf.getKeyPair("ltd-jwt");
jwtAccessTokenConverter.setKeyPair(kp);
return jwtAccessTokenConverter;
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients()
// .tokenKeyAccess("isAuthenticated()")
.checkTokenAccess("permitAll()");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("web")
.secret("web-secret")
.authorizedGrantTypes("password").scopes("web")
.and()
.withClient("andorid")
.secret("andorid-secret")
.authorizedGrantTypes("password").scopes("adnorid");
super.configure(clients);
}
}
这里的JWT不需要保存数据库直接保存在客户端(如果是token的形式验证的话,token一般是存放在redis中)
2、WebSecurityConfigurer:
/**
* @Description: Security配置
*/
@EnableWebSecurity
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
ResourceServerConfig这个类暂时可以不用这是springboot-admin要用的配置类
到这里oauth服务器就写完了。。。可以访问gihub查看源码github传送给,如果感觉可以帮忙点个star
使用postman获取JWT:
参数:
grant_type:password
username:macro
password:macro123
client_id:web
client_secret:web-secret
scope:web
client_id、client_secret和scope需要对应的是AuthorizationServerConfigurer类中的
这里使用的是spring gateway自己测试的时候可以直接访问/oauth/token接口参数不变
测试JWT的有效性:
1、创建测试项目这里使用的是platform-user模块测试
创建的测试项目中配置文件添加:
这里由于使用的是JWT所以不需要访问oauth服务器验证所以也就不需要配置oauth
项目添加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
添加ResourceServerConfig权限验证类、由于oauth用的是密钥的方式生成的JWT所以在微服务中需要用到密钥的公钥解析JWT不然是无法验证的本项目中提供了密钥和公钥可以直接使用
使用JWT访问测试模块的接口:
访问成功。这里使用了spring gateway自己测试的时候可以创建一个普通的springboot项目直接测试
这里所有的源码都在gihub上,喜欢的可以下载下来测试学习。项目中包括了微服务中的eureka注册中心、spring gateway网关、oauth2.0安全验证、springboot-admin服务监控、openfeign远程调用、zipkin+Sleuth分布式链路追踪、jta-atomikos分布式事务处理(后期会改成阿里分布式事务seata)等