Web应用的创建笔记(4)_增加用户认证与授权功能

Web应用的一个通用的需求是能提供用户的认证与授权服务。目前很流行的一个认证授权协议是OAUTH2.0,像我们经常使用的微信,QQ,微博等都是遵循了OAUTH2.0的协议标准,第三方应用可以获得微信,微博的用户授权,读取用户的相关资料。

Keycloak是一个遵循了OAUTH2.0协议的一个开源的应用,在本文中,我将采用Keycloak来保护我之前创建的WEB前端与后端的应用。之前我们搭建的WEB应用是提供了浏览全部产品信息,创建/删除/修改产品的功能。现在我将要设计网络安全的保护策略。具体来说会设置两个用户,一个用户是具备Manager的权限,可以使用所有的功能。另外一个用户是普通用户,只能浏览产品信息,但是不能修改产品。下面将分为三部分来完成这个权限保护的设计。

Keycloak的安装与配置:

首先我们要新建一个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配置文件,这里有个地方要注意,文档中提到要增加

<https-listener name="https" socket-binding="https" security-realm="UndertowRealm"/>
但是这个配置会和已有的以下配置冲突,因此要把已有的这个配置注释掉,不然Keycloak启动会报错。
<https-listener name="https" socket-binding="https" security-realm="ApplicationRealm"/>

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应用的前后端。

  • 对于myweb-backend,Settings的设置如下:

在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。

扫描二维码关注公众号,回复: 3565053 查看本文章
  • 对于myweb-frontend,Settings的设置如下:


WEB后端的配置:

对于Spring boot Rest API的Web后端,在pom.xml中,增加Keycloak的相关依赖,注意以下的配置和Keycloak官网的有些不同,官网的是keycloak-spring-boot-starter,这个和Spring boot 2.0版本会有问题。

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-tomcat8-adapter</artifactId>
    <version>4.0.0.Beta2</version>
</dependency>
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-spring-boot-2-starter</artifactId>
</dependency>

以及以下的

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.keycloak.bom</groupId>
            <artifactId>keycloak-adapter-bom</artifactId>
            <version>4.0.0.Beta2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

在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");
            }
        };
    }
}

现在后端的配置已经做好了。

WEB前端的配置:

对于Angular 5的WEB前端应用,要做以下的修改:

对于Index.html文件,在header里面加入对Keycloak java script adapter的引用:

<script src="https://instance-j593q3gq:8543/auth/js/keycloak.js"></script>

增加一个keycloakservice,如以下代码:

import { Injectable } from '@angular/core';

declare var Keycloak: any;

@Injectable()
export class KeycloakService {
  static auth: any = {};

  constructor() { }

  static init(): Promise<any>{
    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<string>{
    return new Promise<string>((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<any>, next: HttpHandler):
    Observable<HttpEvent<any>> {
      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来显示合适的内容:

<nav>
  <a routerLink="/products">Explore Products</a>
  <div *ngIf="isManager()">
    <a routerLink="/createproduct">Create Product</a>
  </div>
</nav>
<router-outlet></router-outlet>
最后,对main.ts文件,增加以下代码,使得Keycloak service成功加载并完成用户认证后,才加载appmodule:
KeycloakService.init()
  .then(() => platformBrowserDynamic().bootstrapModule(AppModule))
  .catch(e => {
    console.error(e);
});
现在所有的配置都完成了,打开前端的网址,用之前创建的用户来进行登录,检验一下用户认证授权是否正常工作吧。


猜你喜欢

转载自blog.csdn.net/gzroy/article/details/80445223