Untuk mengamankan route pada aplikasi web, kita harus mengerjakan pada sisi frontend dan backend.
Frontend
Pada frontend kita buat class component Login. Buat file baru src/components/Login.js, dengan code seperti berikut:
import React, {Component, Fragment} from "react";
export default class Login extends Component{
constructor(props){
super(props);
this.state={
email : "",
pswrd : "",
error: null,
errors: [],
alert:{
type: "d-none",
msg: "",
}
}
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange = (evt) =>{
let value = evt.target.value;
let name = evt.target.name;
this.setState((prevState)=>({
...prevState,
[name] : value
}))
}
handleSubmit = (evt) =>{
evt.preventDefault();
}
hasError(key){
return this.state.errors.indexOf(key) !== -1;
}
render(){
return(
<Fragment>
<h2>Login</h2>
<div className={`alert ${this.state.alert.type}`} role="alert">{this.state.alert.msg}</div>
<form className="pt-3" onSubmit={this.handleSubmit}>
<div className="mb-3">
<label htmlFor="email" className="form-label">Email</label>
{/* <input type="email" className={this.hasError("email")?"form-control is-invalid":"form-control"} id="email" name="email" onChange={this.handleChg}/> */}
<input type="email" className={`form-control ${this.hasError("email")?"is-invalid":""}`} id="email" name="email" onChange={this.handleChg}/>
<div className={this.hasError("email")?"text-danger":"d-none"}>Please enter email...</div>
</div>
<div className="mb-3">
<label htmlFor="pswrd" className="form-label">Password</label>
<input type="password" className={`form-control ${this.hasError("pswrd")?"is-invalid":""}`} id="pswrd" name="pswrd" onChange={this.handleChg}/>
<div className={this.hasError("pswrd")?"text-danger":"d-none"}>Please enter password...</div>
</div>
<hr/>
<button className="btn btn-primary">Login</button>
</form>
</Fragment>
);
}
}
Pada app.js akan kita lakukan perubahan, dengan skenario, Jika user belum login, maka menu add anime dan manage anime tidak ditampilkan dan pada top menu tampilkan link login.
Dan jika user sudah login dan memiliki akses, maka pada top menu ditampilkan link login dan menu add dan manage anime ditampilkan.
Karena app.js akan menangani state, maka perlu diubah dari fungsi component menjadi class component.
Berikut modifikasi pada file src/App.js.
import React, { Component, Fragment } from 'react';
import {BrowserRouter as Router, Switch, Route, Link} from 'react-router-dom';
import Home from './components/Home';
import Animes from './components/Animes';
import Admin from './components/Admin';
import Genres from './components/Genres';
import Anime from './components/Anime';
import Genre from './components/Genre';
import AddEditAnime from './components/AddEditAnime';
import Login from './components/Login';
export default class App extends Component {
constructor(props){
super(props);
this.state={
jwt : "",
}
this.handleJWT(this.handleJWT.bind(this));
}
handleJWT = (jwt) =>{
this.setState({jwt:jwt});
}
logout = () =>{
this.setState({jwt:""});
}
render(){
let loginLink;
if(this.state.jwt===""){
loginLink = <Link to="/login">Login</Link>
}else{
loginLink = <Link to="/logout" onClick={this.logout}>Logout</Link>
}
return (
<Router>
<div className="container">
<div className="row">
<div className="col mt-3">
<h1 className="mt-3">Anime Collection</h1>
</div>
<div className="col mt-3 text-end">
{loginLink}
</div>
<hr className="mb-3"/>
</div>
<div className="row">
<div className="col-md-2">
<nav>
<ul className="list-group">
<li className="list-group-item"><Link to="/">Home</Link></li>
<li className="list-group-item"><Link to="/animes">Anime</Link></li>
<li className="list-group-item"><Link to="/genres">Genre</Link></li>
{this.state.jwt !== "" &&
<Fragment>
<li className="list-group-item"><Link to="/admin/anime/0">Add Anime</Link></li>
<li className="list-group-item"><Link to="/admin">Manage Anime</Link></li>
</Fragment>
}
</ul>
</nav>
</div>
<div className="col-md-10">
<Switch>
<Route path="/animes/:id" component={Anime} />
<Route path="/animes"><Animes /></Route>
<Route path="/genre/:id" component={Genre} />
<Route exact path="/login" component={(props) => <Login {...props} handleJWT={this.handleJWT}/>}/>
<Route exact path="/genres"><Genres /></Route>
<Route path="/admin/anime/:id" component={AddEditAnime} />
<Route path="/admin"><Admin /></Route>
<Route path="/"><Home /></Route>
</Switch>
</div>
</div>
</div>
</Router>
);
}
}
Backend
Library yang perlu diinstall adalah bcrypt dan jwt.
go get github.com/pascaldekloe/jwt
go get golang.org/x/crypto/bcrypt
Pada file main.go dilakukan modifikasi penambahan config untuk menangani jwt.
Perhatian untuk menyederhanakan tutorial, JWT secret jadi masih di hardcode. Digenerate manual di https://play.golang.org/p/s8KlqJIOWej
Buka file main.go, ubah bagian config struct dan config inisialisasi.
type config struct {
port int
env string
db struct {
dsn string
}
jwt struct {
secret string
}
}
flag.StringVar(&cfg.jwt.secret, "jwt", "2dce505d96a53c5768052ee90f3df2055657518dad489160df9913f66042e160", "secret")
Pada file routes.go tambahkan routing baru untuk menangani user signin.
router.HandlerFunc(http.MethodPost, "/v1/signin", app.Signin)
Buat model untuk user, buka file models/models.go
type User struct {
ID int
Email string
Pswrd string
}
Buat file baru cmd/api/tokens.go
Perhatian data user di hardcode, agar tutorial lebih sederhana. Pada prakteknya akan digunakan database untuk menyimpan informasi user.
Password adalah password yang di hash. Lihat dilink berikut https://play.golang.org/p/uKMMCzJWGsW
package main
import (
"backend/models"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/pascaldekloe/jwt"
"golang.org/x/crypto/bcrypt"
)
var validUser = models.User{
ID: 1,
Email: "test@test.com",
Pswrd: "$2a$12$J797QGR8wqvROqXBl3LFc.axk/kfrV/YjqbBC19.UtzqS9RGsfJSW",
}
type Credentials struct {
UsrNm string `json:"email"`
Pswrd string `json:"pswrd"`
}
func (app *application) Signin(rw http.ResponseWriter, r *http.Request) {
var creds Credentials
err := json.NewDecoder(r.Body).Decode(&creds)
if err != nil {
app.errorJSON(rw, errors.New("unauthorized"))
return
}
hashedPswrd := validUser.Pswrd
err = bcrypt.CompareHashAndPassword([]byte(hashedPswrd), []byte(creds.Pswrd))
if err != nil {
app.errorJSON(rw, errors.New("unauthorized"))
return
}
var claims jwt.Claims
claims.Subject = fmt.Sprint(validUser.ID)
claims.Issued = jwt.NewNumericTime(time.Now())
claims.NotBefore = jwt.NewNumericTime(time.Now())
claims.Expires = jwt.NewNumericTime(time.Now().Add(24 * time.Hour))
claims.Issuer = "mydomain.com"
claims.Audiences = []string{"mydomain.com"}
jwtBytes, err := claims.HMACSign(jwt.HS256, []byte(app.config.jwt.secret))
if err != nil {
app.errorJSON(rw, errors.New("error signing"))
return
}
app.writeJSON(rw, http.StatusOK, string(jwtBytes), "response")
}
Pada tahap awal, tampilan web app akan seperti berikut

Pada modul selanjutnya kita akan hubungkan tombol submit dengan API.