一个Spring Boot, JWT,AugularJS接口安全验证的简单例子

最近研究REST接口的无状态安全验证,这个文章有一定参考价值,但相当不完善,token只是简单用了服务器回传的, 没有实现数据签名和防篡改,另外git代码也有问题, 我简单修改了,可以看到文章中的效果。

我修改代码的git地址: https://github.com/offbye/jwt-angular-spring

原文地址  http://niels.nu/blog/2015/json-web-tokens.html

28 January 2015

When you create a REST service you will probably run into the issue of adding user authentication/authorization to it somewhere down the line. So, how to do it? Of course you can send basic HTTP auth headers (username/password)
for every request but that would require you to keep those credentials in memory and the service would have to check those credentials (including hashing the passwords which should be an expensive operation). So that’s not the best idea.

This is why REST services typically use a token system. A standard token system returns a ‘token‘ (just a long unique string of random characters, for example a GUID) on succesful login. The client in turn then
sends this token in every request’s Authorization header. The service, on every request, ‘rehydrates‘ its context by looking up the context on the server side. This context can be stored in a DB, retrieved from a Redis cache or simply stored in memory in a
hash table. The downside of this approach is that for every REST method you will need to do this lookup in the database or cache.

Enter JSON Web Tokens, or JWT in short. JSON Web Tokens are tokens that are not only unique to a user but also contain whatever information you need for that user, the so called claims. The most basic claim is the
‘subject‘ (basically a unique user ID) but the tokens can be extended to include any information you want. Examples would be api access rights or user roles; you can simply add a ‘roles‘ array with the ‘user‘ and ‘admin‘ rights to the claims when a user logs
in. These claims can then be retrieved from the JWT whenever the client sends the JWT to the server.

Obviously this token is not plain text; that would make it trivial for a client to add an ‘admin‘ claim to it’s set. JWT’s are both encrypted with a secure key (only known to the server) as well as signed. This
way the client can’t decrypt or alter the claims in any way. Only the server can decrypt the claims because only the server has the key.

So enough with the theory; let’s get down to some actual code. I have created a small
example project
 that showcases the JWT exchange between a Java back-end (based on Spring Boot) and a JavaScript (AngularJS) front-end.

You can fork and clone the project to your local machine.
It contains a runnable Spring Web Server (com.nibado.example.jwtangspr.WebApplication) in src/main/java/. You can simply run it in your IDE, set breakpoints, etc. If you check out the java classes you’ll see that it’s a simple Spring Boot REST API. I picked
Spring Boot just because it’s convenient. If you’d want to create a Vert.x Verticle or a plain Servlet that would work just fine too.

I am using the pretty awesome JSON Web Token for Java library to handle the tokens
themselves. Unfortunately it’s not the standard "Java API" you’d find on http://jwt.io/. It should be: it’s much more convenient
to use than the Auth0 library.

First we’ll start with the signing. It is handled by the /user/login route:

    @RequestMapping(value = "login", method = RequestMethod.POST)
    public LoginResponse login(@RequestBody final UserLogin login)
        throws ServletException {
        if (login.name == null || !userDb.containsKey(login.name)) {
            throw new ServletException("Invalid login");
        }
        return new LoginResponse(Jwts.builder().setSubject(login.name)
            .claim("roles", userDb.get(login.name)).setIssuedAt(new Date())
            .signWith(SignatureAlgorithm.HS256, "secretkey").compact());
    }

NOTE
As you can see we use a hardcoded key "secretkey" here. In real production scenario’s this would typically be randomly generated on start-up or stored in some kind of central cache. This has the added benefit of making all tokens invalid when the service restarts.
For convenience it’s hardcoded in these examples.

When a user logs in (I’ve defined the users ‘Tom‘ and ‘Sally‘ in my ‘database‘) with a correct password it will return a LoginResponse POJO (which gets translated to JSON automatically by Spring) with a ‘token‘.
This response should look like this:

{
	"token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0b20iLCJyb2xlcyI6WyJ1c2VyIl0sImlhdCI6MTQyMjQ1NjY0N30.msySR65rhvfIrMsS8AiuarvCx8c14gd_5qFuA3NA2R4"
}

This ‘token‘ is then used on subsequent API calls from the client to the server. The standard approach here is to send an Authorization header with a "Bearer" token. The header would thus be

Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0b20iLCJyb2xlcyI6WyJ1c2VyIl0sImlhdCI6MTQyMjQ1NjY0N30.msySR65rhvfIrMsS8AiuarvCx8c14gd_5qFuA3NA2R4

As you might have noticed the WebApplication configures a Filter. Servlet filters can do all kinds of things with and to HttpRequests, we will be using this filter to protect our ‘api‘ endpoints. As you can see
the WebApplication class configures our JwtFilter to act only on "/api/*" endpoints:

        final FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(new JwtFilter());
        registrationBean.addUrlPatterns("/api/*");

This way it won’t complain when we call /user/login without an Authorization header. Another approach would be to protect every end-point and just ignore certain paths in the filter itself.

The filter is responsible for checking if the correct Authorization header is there and if the token in the header is valid:

    @Override
    public void doFilter(final ServletRequest req,
                         final ServletResponse res,
                         final FilterChain chain) throws IOException, ServletException {
        final HttpServletRequest request = (HttpServletRequest) req;

        final String authHeader = request.getHeader("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            throw new ServletException("Missing or invalid Authorization header.");
        }

        final String token = authHeader.substring(7); // The part after "Bearer "

        try {
            final Claims claims = Jwts.parser().setSigningKey("secretkey")
                .parseClaimsJws(token).getBody();
            request.setAttribute("claims", claims);
        }
        catch (final SignatureException e) {
            throw new ServletException("Invalid token.");
        }

        chain.doFilter(req, res);
    }

We use the Jwt parser to decrypt the token with the same key we used to encrypt it. If the key is valid we then store the "Claims" that contains the user name and roles in the request object so it can be used by
API endpoints down the line.

Last but not least we call chain.doFilter so that whatever is configured down the line also gets called. Without it the request would not be passed onto our controllers.

So now that we have this filter in place we can actually define a nice super safe API method:

    @RequestMapping(value = "role/{role}", method = RequestMethod.GET)
    public Boolean login(@PathVariable final String role,
                         final HttpServletRequest request) throws ServletException {
        final Claims claims = (Claims) request.getAttribute("claims");

        return ((List<String>) claims.get("roles")).contains(role);
    }

NOTE
Since this URL matches the "api/*" rule it is protected against unauthorized users. In all the "api/" endpoints we can assume the user is authorized.

This /api/role/{rolename} will respond with ‘true‘ or ‘false‘ depending on if our Claims have the role we ask for. Here the benefit of using JWT over regular tokens becomes obvious: the roles for a user is contained
inside the JWT, there is no need to rehydrate a contex by doing a database lookup. This makes scaling webservices much easier since you won’t risk bottlenecking your services on DB lookups.

If you’re currently running the WebApplication it should serve the static files too. If you point your browser to http://localhost:8080/index.html you
should see a welcome message (Welcome to the JSON Web Token / AngularJR / Spring example!) and a login screen. If you don’t see the welcome message you might want to see if the server is running and check your browser to see if perhaps the external javascript
files are blocked.

Login with either ‘tom‘ or ‘sally‘ and you should see what roles they have. Lets take a quick look at the source. The application only has a controller and a small service, both in src/main/webapp/app.js. We’ll
start with the service:

appModule.service(‘mainService‘, function($http) {
    return {
        login : function(username) {
            return $http.post(‘/user/login‘, {name: username}).then(function(response) {
                return response.data.token;
            });
        },

        hasRole : function(role) {
            return $http.get(‘/api/role/‘ + role).then(function(response){
                return response.data;
            });
        }
    };
});

It wraps the two available REST calls into corresponding functions. Not much to see here, so lets move on to the controller:

appModule.controller(‘MainCtrl‘, [‘mainService‘,‘$scope‘, ‘$http‘,
        function(mainService, $scope, $http) {
            $scope.greeting = ‘Welcome to the JSON Web Token / AngularJR / Spring example!‘;
            $scope.token = null;
            $scope.error = null;
            $scope.roleUser = false;
            $scope.roleAdmin = false;
            $scope.roleFoo = false;

            $scope.login = function() {
                $scope.error = null;
                mainService.login($scope.userName).then(function(token) {
                    $scope.token = token;
                    $http.defaults.headers.common.Authorization = ‘Bearer ‘ + token;
                    $scope.checkRoles();
                },
                function(error){
                    $scope.error = error
                    $scope.userName = ‘‘;
                });
            }

            $scope.checkRoles = function() {
                mainService.hasRole(‘user‘).then(function(user) {$scope.roleUser = user});
                mainService.hasRole(‘admin‘).then(function(admin) {$scope.roleAdmin = admin});
                mainService.hasRole(‘foo‘).then(function(foo) {$scope.roleFoo = foo});
            }

            $scope.logout = function() {
                $scope.userName = ‘‘;
                $scope.token = null;
                $http.defaults.headers.common.Authorization = ‘‘;
            }

            $scope.loggedIn = function() {
                return $scope.token !== null;
            }
        } ]);

The controller has some state and a few functions. They should be self-explanatory aside from the $http part. What you see here is that on successful login the Authorization header is set forall $http
requests. On the other hand the logout() function clears the token and the header so the user will be logged out. This of course is a blanket-approach since it applies to all $http requests. If you only want to use the header in some requests you will have
to handle this in the service.

If you open your browser’s developer console and keep an eye on the network tab you will be able to see all the AJAX calls being done. You can also use a REST client like Postman to
do the calls yourself.

This concludes this little tutorial on JSON web tokens integrated on both the server and the client side. If you have any questions or remarks feel free to contact me. You can find my contact details in the "about"
page.

版权声明:本文为博主原创文章,转载请保留出处http://blog.csdn.net/offbye

时间: 2024-10-11 10:30:57

一个Spring Boot, JWT,AugularJS接口安全验证的简单例子的相关文章

spring boot+jwt 权限验证

上周看了一下jwt以前公司的开发都是使用session共享的方法.现在主流的两种方式一种是把登录信息保存在服务器,另一种则是把信息保存在客户端.在使用session 存储的时候会遇到很多的问题,随着项目越来越多工作量会变得越来越大.现在公司要开始一个新的项目,索性就开始使用jwt,数据保存在客户端每一次请求都回传给服务器验证一下. 本文分为两部分第一部分简单介绍一下JWT,第二部分简单介绍一下使用spring boot+jwt构建一个项目. 一.什么是JWT? JWT全程JSON Web tok

使用docker构建第一个spring boot项目

在看了一些简单的docker命令之后 打算自己尝试整合一下docker+spring boot项目本文是自己使用docker+spring boot 发布一个项目1.docker介绍 docke是提供简单易用的容器接口Docker 将应用程序与该程序的依赖,打包在一个文件里面.运行这个文件,就会生成一个虚拟容器.程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样.有了 Docker,就不用担心环境问题. 总体来说,Docker 的接口相当简单,用户可以方便地创建和使用容器,把自己的应用放入

快速构建一个Spring Boot+MyBatis的项目IDEA(附源码下载)

如何快速构建一个Spring Boot的项目 工具 idea JDK版本 1.8 Spring Boot 版本 1.5.9 环境搭建实现:最基础前端可以访问到数据库内的内容 开始 IDEA 内部新建一个项目,项目类型选择Spring Initializr,Project SDK选择适合你当前环境的版本,这里我选择的是1.8(Spring Boot 2.0以上的版本,JDK选择请选择1.8即以上版本),构建服务选择默认就好,点击Next 填写Group和Artifact(此处我使用的是默认,请根据

第一个Spring Boot应用

Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程.该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置.通过这种方式,Spring Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者.----以上来自百度百科. 简单来说,它的一个特点是:习惯大于约定.Spring Boot提倡基于Java的配置,注解将会代替很多的配置文件.以下使用Maven

我的第一个spring boot程序(spring boot 学习笔记之二)

第一个spring boot程序 写在前面:鉴于spring注解以及springMVC的配置有大量细节和知识点,在学习理解之后,我们将直接进入spring boot的学习,在后续学习中用到注解及其他相关知识点时会再次理解.要运行起第一个Spring boot特别简单,用IDEA包含了Spring Boot的引导,直接新建一个spring boot项目. 注意: 1.第一次新建Spring boot项目的时候,maven会下载大量的依赖到本地,所以特别慢,耐心等待或者用国内的maven公库都行(自

用 Docker、Gradle 来构建、运行、发布一个 Spring Boot 应用

本文演示了如何用 Docker.Gradle 来构建.运行.发布来一个 Spring Boot 应用.Docker 简介Docker 是一个 Linux 容器管理工具包,具备"社交"方面,允许用户发布容器的 image (镜像),并使用别人发布的 image.Docker image 是用于运行容器化进程的方案,在本文中,我们将构建一个简单的 Spring Boot 应用程序.前置条件JDK 1.8+Gradle 2.3+Docker 最新版.有关 Docker 在的安装, 如果你的电

只需两步!Eclipse+Maven快速构建第一个Spring Boot项目

随着使用Spring进行开发的个人和企业越来越多,Spring从一个单一简洁的框架变成了一个大而全的开源软件,最直观的变化就是Spring需要引入的配置也越来越多.配置繁琐,容易出错,让人无比头疼,简化Spring配置简直可以说是民心所向. Spring Boot是由Pivotal团队提供的一个基于Java的全新的开源框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程.该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置.如今,Spring Boot逐渐成为快

02-第一个Spring Boot应用程序

Spring Boot深度课程系列 02第一个Spring Boot应用程序 1.版本要求 集成开发环境:IntelliJ IDEA 2017.2.1 ,Spring Boot 版本:2.2.42. 2.步骤介绍 3.编写Helloworld,参照Spring MVC的写法 A) 在chapter01文件夹下创建包controller,创建类HelloController. B) 代码如下 package com.java.chapter01.controller; import org.spr

最近做了一个Spring Boot小项目,大家帮忙找找bug吧, http://www.dbeetle.cn

最近做了一个Spring Boot小项目,网站顶部有源码地址,欢迎大家访问 http://www.dbeetle.cn 欢迎各位访问,提出意见,找找bug 网站说明 甲壳虫社区(Beetle Community) 一个开源的问答社区.论坛博客,您可以提出自己的问题.发布自己的文章.和其他用户交流 目前功能有第三方登陆.查看.发布.评论.消息通知.顶置.一键已读.搜索等 后续会不断更新完善,欢迎大家提供更好的建议 使用技术 Spring Boot.Mybatis.Thymeleaf.BootStr