Web应用的一个通用的需求是能提供用户的认证与授权服务。目前很流行的一个认证授权协议是OAUTH2.0,像我们经常使用的微信,QQ,微博等都是遵循了OAUTH2.0的协议标准,第三方应用可以获得微信,微博的用户授权,读取用户的相关资料。
Keycloak是一个遵循了OAUTH2.0协议的一个开源的应用,在本文中,我将采用Keycloak来保护我之前创建的WEB前端与后端的应用。之前我们搭建的WEB应用是提供了浏览全部产品信息,创建/删除/修改产品的功能。现在我将要设计网络安全的保护策略。具体来说会设置两个用户,一个用户是具备Manager的权限,可以使用所有的功能。另外一个用户是普通用户,只能浏览产品信息,但是不能修改产品。下面将分为三部分来完成这个权限保护的设计。
首先我们要新建一个Keycloak的服务。按照Keycloak.org官网的介绍,安装并启动Standalone Mode,按照安装文档第7章的网络配置,绑定好IP地址和开启HTTPS。我的Keycloak是建在云主机上,这样可以通过外网来访问。要注意的是,在开启HTPPS的同时,也要按照官网的Server Installation文档中的7.3.1 Enabling SSL/HTTPS for Keycloak Server中的说明来配置服务器证书和导入到JDK的Keystore。我是采用生成自签名证书的方式,其中在生成证书的时候,Common Name要设置为服务器的hostname,如果这个不设置,那么之后证书在Spring Boot的应用中会报错,说服务器的IP地址没有找到对应的Alternate Name。证书生成之后,还要按照文档的说明,修改Keycloak的configuration目录下对应的XML配置文件,这里有个地方要注意,文档中提到要增加
但是这个配置会和已有的以下配置冲突,因此要把已有的这个配置注释掉,不然Keycloak启动会报错。
Keycloak设置好之后就可以输入以下指令运行了,我采用的是Standalone的方式来运行:
bin/standalone.sh -b 192.168.0.2
这时在本地的机器上,配置hosts文件,把Keycloak服务器的主机名和IP地址加进去。然后打开浏览器,输入https://hostname:8443/auth,就可以打开Keycloak的页面了。
在Keycloak里面,我们新增一个Realm,名字叫Demo。然后在这个Demo Realm里面,增加2个用户,一个是roy,一个是test。增加2个Realm role,一个是manager,一个是user。把manager和user这2个role都赋予给用户roy,把user这个role赋予给用户test。在clients里面增加2个应用,一个是myweb-backend,一个是myweb-frontend,分别对应web应用的前后端。
在Settings里面,我们打开了Authorization的选项,接着在Authorization的Resources里面,把后端提供的几个API的URI作为Resourse配置进去,具体配置如下图:
然后,在Policies里面,配置2个Police,名字分别是Only Manager和Only User,这两个Police都是基于Role的,分别为其分配对应的manager和user realm role
最后,在Permissions里面,创建一个名字为Only Manager Permission的Permission,Apply to Resource Type设置为on,在Resource type里面输入Manager Access Only,Apply Policy里面选择Only manager。另外再创建一个User and Manager Permission的Permission,Apply to Resource Type设置为off,在Resource里面输入All products,Apply Policy里面选择Only manager和Only user。
对于Spring boot Rest API的Web后端,在pom.xml中,增加Keycloak的相关依赖,注意以下的配置和Keycloak官网的有些不同,官网的是keycloak-spring-boot-starter,这个和Spring boot 2.0版本会有问题。
org.keycloak
keycloak-tomcat8-adapter
4.0.0.Beta2
org.keycloak
keycloak-spring-boot-2-starter
以及以下的
org.keycloak.bom
keycloak-adapter-bom
4.0.0.Beta2
pom
import
在application.properities文件中,增加以下的配置信息,这个配置信息是适合Access Type为Credential类型的Cleint的,如果是Bearer Only类型的Client,那么在配置信息里面的keycloak.securityConstraints.authRoles和patterns里面需要配置对哪个Role对哪个Role有访问权限,这样如果以后要改动配置权限还需要对应用程序做修改,不够灵活。如果是Confidential类型,那么直接在Keycloak里面修改Policy,Permission即可,不需要再改动应用程序。
keycloak.auth-server-url=https://instance-j593q3gq:8543/auth
keycloak.realm=demo
keycloak.ssl-required = external
#Follow two lines are for "bearer only" client.
#keycloak.realmKey=XXXXX
#keycloak.bearer-only=true
keycloak.resource=myweb-backend
#Follow line for "credential" client.
keycloak.credentials.secret=XXXXXX
keycloak.securityConstraints[0].authRoles[0]=user
keycloak.securityConstraints[0].securityCollections[0].name=protected
keycloak.securityConstraints[0].securityCollections[0].patterns[0]=/*
keycloak.policy-enforcer-config.on-deny-redirect-to=/accessDenied
keycloak.policy-enforcer-config={}
#Keycloak Enable CORS
keycloak.cors = true
增加一个配置类,解决跨域访问的问题:
package com.example.myweb;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class CorsConfig {
@Bean
public FilterRegistrationBean corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("HEAD");
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("PATCH");
source.registerCorsConfiguration("/**", config);
// return new CorsFilter(source);
final FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
@Bean
public WebMvcConfigurer mvcConfigurer() {
return new WebMvcConfigurerAdapter() {
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("GET", "PUT", "POST", "GET", "OPTIONS");
}
};
}
}
现在后端的配置已经做好了。
对于Angular 5的WEB前端应用,要做以下的修改:
对于Index.html文件,在header里面加入对Keycloak java script adapter的引用:
增加一个keycloakservice,如以下代码:
import { Injectable } from '@angular/core';
declare var Keycloak: any;
@Injectable()
export class KeycloakService {
static auth: any = {};
constructor() { }
static init(): Promise{
const keycloak = Keycloak({
url: 'https://instance-j593q3gq:8543/auth',
realm: 'demo',
clientId: 'myweb-frontend'
});
return new Promise((resolve, reject) => {
keycloak
.init({ onLoad: 'login-required' })
.success(() => {
KeycloakService.auth.authz = keycloak;
resolve();
})
.error(() => {
reject();
});
});
}
static getToken(): Promise{
return new Promise((resolve, reject) => {
if (KeycloakService.auth.authz.token) {
KeycloakService.auth.authz
.updateToken(90) // refresh token if it will expire in 90 seconds or less
.success(() => {
return resolve(KeycloakService.auth.authz.token);
})
.error(() => {
return reject('Failed to refresh token');
});
} else {
return reject('Not logged in');
}
});
}
static hasAnyRole(roles: String[]): boolean {
for (let i = 0; i < roles.length; i++) {
if (KeycloakService.hasRole(roles[i])) {
return true;
}
}
return false;
}
static hasRole(role: String): boolean {
return KeycloakService.auth.authz.hasRealmRole(role);
}
}
再增加一个http auth service,其作用是作为一个拦截器,在每一个http request的头部增加token的信息:
import { Injectable } from '@angular/core';
import {
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';
import { KeycloakService } from './keycloak.service';
import { Observable } from 'rxjs';
@Injectable()
export class HttpAuthService implements HttpInterceptor{
constructor() { }
intercept(req: HttpRequest, next: HttpHandler):
Observable> {
return Observable.fromPromise(KeycloakService.getToken()).switchMap(token=>{
const authReq = req.clone({ setHeaders: { Authorization: 'Bearer '+token } });
return next.handle(authReq);
});
}
}
在Product service中,httpoptions里面增加一个authorization的字段:
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json', 'Authorization': ''})
};
修改app.component.ts文件,增加一个方法,判断用户是否具备manager role:
public isManager(): boolean {
return KeycloakService.hasAnyRole(['manager']);
}
在app.component.html文件中,做以下修改,以便根据用户的role来显示合适的内容:
最后,对main.ts文件,增加以下代码,使得Keycloak service成功加载并完成用户认证后,才加载appmodule:
KeycloakService.init()
.then(() => platformBrowserDynamic().bootstrapModule(AppModule))
.catch(e => {
console.error(e);
});
现在所有的配置都完成了,打开前端的网址,用之前创建的用户来进行登录,检验一下用户认证授权是否正常工作吧。