一个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.


你可能感兴趣的:(spring,Security,Boot,jwt)