Keycloak으로 Svelte 보안
목차를 자유롭게 사용하십시오.
Github에서 코드를 찾을 수 있습니다.
마테부첵 / Secure_Svelte_With_Keycloak
Keycloak으로 Svelte 앱을 보호하는 예
처음부터 시작합시다. 가장 먼저 필요한 것은 Keycloak 서버가 실행되는 것입니다. 최신 버전here을 다운로드하거나 예를 들어 docker를 사용할 수 있습니다.
목차
1. Keycloak 영역 및 클라이언트 설정
Now let's login to our Administration console and create new Realm, Client and some user and role for testing purposes.
Let's start with Realm.Realm을 설정한 후에는 새 클라이언트도 생성해야 이 인증 서버를 사용할 수 있습니다.
이제 새 기본 역할과 새 사용자를 생성합니다.
이제 우리는 다음 단계로 넘어가기에 좋습니다.
2. 새로운 Sapper 프로젝트 준비
At first we have to create new project. I am going to use Sapper and TypeScript, but you can do the same thing with Svelte and JavaScript.
npx degit "sveltejs/sapper-template#rollup" my-app
Now let's convert our project to TypeScript.
node scripts/setupTypeScript.js
The last thing that is left to do is adding Keycloak JS script tag to our template.html and then we can continue to the fun part.
To make our life easier our Keycloak server provides the right version of JS file for us so our tag should look like this.
<script src="https://{your_server_url}/auth/js/keycloak.js"></script>
So our template.html should look something like this.
...
<link rel="stylesheet" href="global.css">
<link rel="manifest" href="manifest.json" crossorigin="use-credentials">
<link rel="icon" type="image/png" href="favicon.png">
<script src="https://{your_server_url}/auth/js/keycloak.js"></script>
<!-- Sapper creates a <script> tag containing `src/client.js`
and anything else it needs to hydrate the app and
initialise the router -->
%sapper.scripts%
...
3. 인증 및 사용자 클래스 만들기
The Auth class will handle most of the stuff. The User class is just so we can easily keep track of actual user.
I am using the default flow, which is the Authorization Flow. But you can obviously change that to e. g. Implicit Flow.
You can find more here in the official documentation .Auth.class.ts - Github의 전체 코드
import { writable } from 'svelte/store';
import { User } from "./User.class";
//Template for loacl storage mapping
export type localStorageMapping = {"access_token": string, "refresh_token": string, "exp": string};
export class Auth {
//The actual keycloak connector
private keycloak: any;
//Used mapping
private localStorageMapping: localStorageMapping;
//This keeps track whether Auth and Role guards can call buildUser method
private initialized: any;
//This class builds the actual User from access token
public buildUser(): User {
let parsed = this.keycloak.tokenParsed;
if(!parsed){
return null;
}
//If you also want the resource roles, just concat them here
return new User(parsed["sub"], parsed["preferred_username"], parsed["given_name"], parsed["family_name"], parsed["realm_access"]["roles"]);
};
public constructor(config: {}, localStorageMapping?: localStorageMapping) {
//Keycloak class is not defined, because we add that library into the template.html
//@ts-ignore
this.keycloak = new Keycloak(config);
this.initialized = writable(false);
if(localStorageMapping){
this.localStorageMapping = localStorageMapping;
}else{
this.localStorageMapping = {
"access_token": "access_token",
"refresh_token": "refresh_token",
"exp": "exp"
};
}
//Check, if user is authenticated
if (localStorage.getItem(this.localStorageMapping.access_token) !== null) {
this.refresh();
}
}
public isInitialized(): any{
return this.initialized;
}
//Makes the initialization process with given parameters
private init(initParams: {}) {
this.keycloak
.init(initParams)
.then((authenticated) => {
if (authenticated) {
localStorage.setItem(
this.localStorageMapping.access_token,
this.keycloak.token
);
localStorage.setItem(
this.localStorageMapping.refresh_token,
this.keycloak.refreshToken
);
localStorage.setItem(this.localStorageMapping.exp, this.keycloak.tokenParsed["exp"]);
//Setting the update (refresh) of our token
this.keycloak.updateToken(5).then((refreshed) => {
if (refreshed) {
localStorage.setItem(
this.localStorageMapping.access_token,
this.keycloak.token
);
localStorage.setItem(
this.localStorageMapping.refresh_token,
this.keycloak.refreshToken
);
localStorage.setItem(this.localStorageMapping.exp, this.keycloak.tokenParsed["exp"]);
}
});
}
this.initialized.set(true);
})
.catch(function (e) {
console.error(e);
});
this.keycloak.onTokenExpired = () => {
//Setting the update (refresh) of our token
this.keycloak.updateToken(5).then((refreshed) => {
if (refreshed) {
localStorage.setItem(
this.localStorageMapping.access_token,
this.keycloak.token
);
localStorage.setItem(
this.localStorageMapping.refresh_token,
this.keycloak.refreshToken
);
localStorage.setItem(this.localStorageMapping.exp, this.keycloak.tokenParsed["exp"]);
}
});
};
}
//This builds initial parameters and add the access token and refresh token.
//You can also use the check-sso. More in official docs.
private buildInitParams(onLoad: string = "login-required", silentCheckSsoRedirectUri?: string): any {
return {
onLoad,
token: localStorage.getItem(this.localStorageMapping.access_token),
refreshToken: localStorage.getItem(this.localStorageMapping.refresh_token),
silentCheckSsoRedirectUri
};
}
public login() {
this.init(this.buildInitParams());
}
public refresh() {
this.init(this.buildInitParams());
}
public logout() {
localStorage.removeItem(this.localStorageMapping.access_token);
localStorage.removeItem(this.localStorageMapping.refresh_token);
localStorage.removeItem(this.localStorageMapping.exp);
this.keycloak.logout();
}
//Checks whether there is the back redirect from auth server
public checkParams(){
let params = (new URL(document.location.href.replace("#", "?"))).searchParams;
if(params.get("state") && params.get("session_state") && params.get("code")){
this.init(this.buildInitParams());
}
}
}
User.class.ts - Github의 전체 코드
export class User{
//The sub parameter of access token
private userId: string;
private username: string;
private firstname: string;
private lastname: string;
private roles: Array<string>;
public constructor(userId: string, username: string, firstname: string, lastname: string, roles: Array<string>){
this.userId = userId;
this.username = username;
this.firstname = firstname;
this.lastname = lastname;
this.roles = roles;
}
//Checks whether user has all of the roles
public hasRole(role: string | Array<string>): boolean{
if(role instanceof Array){
let contains = true;
role.forEach((r) => {
contains = contains && this.roles.includes(r);
});
return contains;
}
return this.roles.includes(role);
}
//getters...
}
4. AuthGuard 및 RoleGuard 구성 요소 생성
Now we will take a look at how we would actually check, whether the user is authenticated or not.
Let's define Svelte component for that purpose.
<script>
import { onMount } from 'svelte';
import { goto } from '@sapper/app';
import { authStore } from './stores';
let auth;
let unsub;
let initialized;
$: if(auth) {
auth.initialized.subscribe(i => {
initialized = i;
});
};
$: user = (initialized) ? auth.buildUser() : null;
let forceLogin = false;
let manual = false;
export {
forceLogin,
manual
}
onMount(() => {
unsub = authStore.subscribe(value => {
auth = value;
});
if(forceLogin && user === null){
goto("/login");
}
});
</script>
{#if user && manual}
<slot name="authed"></slot>
{:else if !user && manual}
<slot name="not_authed"></slot>
{:else if user && !manual}
<slot></slot>
{/if}
Don't worry about that stores.js, we will define it later.
So as you can see it is really simple. You can specify whether you want user to be redirected to login page, if user is not logged in. It also allows you to manually specify which of your tags should be displayed when user is authenticated and when he isn't.
Let me show you little example:
<AuthGuard>
<h1>This will be showed to authenticated user.</h1>
</AuthGuard>
<AuthGuard forceLogin=true>
<h1>This will force user to login.</h1>
</AuthGuard>
<AuthGuard manual=true>
<h1 slot="authed">This will be showed to authenticated user.</h1>
<h1 slot="not_authed">This will be showed to not authenticated user.</h1>
</AuthGuard>
Now let's take look on RoleGuard. This component will help you check whether user has right roles.
<script>
import { onMount } from "svelte";
import { authStore } from './stores';
let auth;
let unsub;
let initialized;
$: if(auth) {
auth.initialized.subscribe(i => {
initialized = i;
});
};
$: user = (initialized) ? auth.buildUser() : null;
let roles;
let actualRoles = roles.split(",");
let manual = false;
export {
roles,
manual
}
onMount(() => {
unsub = authStore.subscribe((value) => {
auth = value;
});
});
</script>
{#if user}
{#if user.hasRole(actualRoles) && manual}
<slot name="role"></slot>
{:else if !user.hasRole(actualRoles) && manual}
<slot name="no_role"></slot>
{:else if user.hasRole(actualRoles) && !manual}
<slot></slot>
{/if}
{/if}
Let's take look on another example:
<RoleGuard roles=user>
<h2>You have user role!</h2>
</RoleGuard>
<RoleGuard roles=user,admin>
<h2>You have user and admin roles!</h2>
</RoleGuard>
<RoleGuard manual=true roles=user>
<h2 slot=role>You have user role!</h2>
<h2 slot=no_role>You don't have user role!</h2>
</RoleGuard>
Now we have our two main components. In next section we will take look on stores.js and setting up our Keycloak JS adapter.
5. 클래스와 컴포넌트 사용하기
At this point we have all components, Auth class and User class. What is left thou is stores.js and _layout.svelte. So let's take look at them now.
stores.js
import { writable } from 'svelte/store';
export const authStore = writable(null);
This store allow us to share one Auth instance between all of our components.
_layout.svelte
<script lang="ts">
import { onMount } from "svelte";
import { Auth } from "../components/Auth.class";
import { authStore } from "../components/stores";
import Nav from "../components/Nav.svelte";
onMount(() => {
authStore.set(
new Auth({
realm: "{realm_name}",
"auth-server-url": "{your_server_url/auth}",
"ssl-required": "external",
resource: "{resource}",
clientId: "{client_id}",
"public-client": true,
"confidential-port": 0,
})
);
});
export let segment: string;
</script>
<style>
main {
position: relative;
max-width: 56em;
background-color: white;
padding: 2em;
margin: 0 auto;
box-sizing: border-box;
}
</style>
<Nav {segment} />
<main>
<slot />
</main>
Here's what it all connects together. Layout allows us to define one Auth instance and distribute it through our other components. We can now specify our realm and client we'v created at beginning.
If you want to know other attributes you can specify take look at official documentation.
Now let me show you example of a login page.
<script>
import { onMount } from 'svelte';
import { authStore } from '../components/stores';
import AuthGuard from '../components/AuthGuard.svelte';
let unsub;
let auth;
onMount(() => {
unsub = authStore.subscribe(
(a) => {
auth = a;
}
);
});
$: if(auth){
auth.checkParams();
};
function login(){
if(auth){
auth.login();
}
}
function logout(){
if(auth){
auth.logout();
}
}
</script>
{#if auth}
<AuthGuard manual="true">
<button slot="not_authed" on:click={login}>Login</button>
<button slot="authed" on:click={logout}>Logout</button>
</AuthGuard>
{/if}
It's pretty simple, just calling methods we'v defined in Auth class.
After successful login you can try to go to developer tools, take your access token and paste it to JWT.IO 그러면 토큰이 구문 분석된 것을 볼 수 있습니다.전체 구현을 보려면take look at my Github .
6. 결론
In this post we'v taken a look at simple way of securing Svelte/Sapper applications. This is my first post here so I hope it was at least helpful.
Thank you very much for reading. Feel free to ask me any question or give me some suggestion.
자원
* Keycloak Docs
Reference
이 문제에 관하여(Keycloak으로 Svelte 보안), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/matejbucek/secure-svelte-with-keycloak-42g3텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)