`

Simplified Spring Security with Grails(转)

阅读更多

Spring Security is a powerful library for securing your applications that comes with a bewildering number of options. Based on Spring, it can be readily integrated into a Grails application. But why not save the hassle and use the new improved Grails plugin ?

The plugin has gone through several evolutionary stages that started with the Acegi plugin. Its most recent incarnation is a complete rewrite for Spring Security 3 and Spring 3. One of the results of this is that the plugin will only work with Grails 1.2.2 and above. Another significant change is that there is no longer just one Spring Security plugin: some features have been broken out into optional plugins. So now you only include the features you need in your application.

So what do the plugins give you? The core provides the basics necessary for access control in an easy-to-use package based on users and roles. In fact, many applications won't need any other plugin than the core one. For those who do need something extra, here is a list of the other plugins in the family:

  • OpenID – authentication using OpenID
  • LDAP – authentication against LDAP servers
  • CAS – single sign-on using CAS
  • ACLs – access control via Spring Security's ACLs
  • UI – user interface for user and role managment, plus other features

In this article I'll show you how to secure a Grails application from scratch using the new core plugin

 

Setup

As with most plugins, your first step will be to install the Spring Security plugin. Of course, you'll need a project to install it into and for this article I have provided a simple Twitter-clone called Hubbub (based on the sample application from Grails in Action). You can also get the finished project from here .

So, from within your project, run:

grails install-plugin spring-security-core
 

If you look at the output generated by the plugin installation, you will see that it provides a couple of commands. The most important of these is s2-quickstart , which will help you get up and running with the minimum of fuss. It generates both the basic domain classes you need to store user information and the controllers that handle authentication.

Before you run the command, you may need to make a decision. If you already have a 'user' domain class, you will have to decide how to integrate it with the one generated by the plugin. One option is to replace the existing domain class and simply apply your customisations to the replacement. The other approach consists of making your own domain class extend the plugin's one.

Which is better? I prefer the latter because it allows you to easily update the generated user domain class if its template ever changes. It also means you don't overly pollute your domain model with Spring Security specifics. On the downside, you have to deal with domain class inheritance, although the cost is pretty minimal.

For Hubbub, we'll make the user domain class extend the generated one, which means we should use domain class names that don't conflict with the existing ones:

grails s2-quickstart org.example SecUser SecRole
 

This will create three domain classes for us:

  • org.example.SecUser
  • org.example.SecRole
  • org.example.SecUserSecRole – links users to roles

and two controllers:

  • LoginController
  • LogoutController

plus their associated views. In just two commands, we have everything we need to start securing our application!

The sample application does need one more change: its URL mappings mean that the login and logout controllers can't be reached. That's simple enough to fix by adding the following two lines to UrlMappings.groovy :

"/login/$action?"(controller: "login")
"/logout/$action?"(controller: "logout")
 

If you don't make the change, the login page will generate a 404 error! Now let's get down to the business of protecting the application.

Adding access control

The whole point of this exercise is to limit access to certain parts of the application. For web applications this most commonly means protecting particular pages, or more specifically URLs. In the case of Hubbub, we have the following requirements:

  • The home page is accessible to everyone – /
  • Only known users can see the posts for a particular user – /person/<username>
  • Only users with a 'user' role should be able to access their own timeline – /timeline
  • Same goes for following another user – /post/followAjax
  • Only fully authenticated users with the 'user' role should be able to post a new message – /post/addPostAjax

With the Spring Security plugin this is trivial to achieve, although you do have to make a decision on which of three mechanisms to use. You can take a controller-centric approach and annotate the actions; work with static URL rules in Config.groovy ; or define runtime rules in the database using request maps.

Annotations

For a controller-centric approach, you can't beat the @Secured annotation provided by the plugin. In it's simplest incarnation, you pass it a list of basic rules that define who can access the corresponding action. Here, I apply Hubbub's access control rules via annotations on the post controller:

package org.example

import grails.plugins.springsecurity.Secured

class PostController {
    ...
    @Secured(['ROLE_USER'])
    def followAjax = { ... }

    @Secured(['ROLE_USER', 'IS_AUTHENTICATED_FULLY'])
    def addPostAjax = { ... }

    def global = { ... }

    @Secured(['ROLE_USER'])
    def timeline = { ... }

    @Secured(['IS_AUTHENTICATED_REMEMBERED'])
    def personal = { ... }
}
 

The IS_AUTHENTICATED_* rules are built into Spring Security, but ROLE_USER is a role that must exist in the database – something we have yet to do. Also, if you specify more than one rule in the list, then the current user normally only has to satisfy one of them – as is explained in the user guide. IS_AUTHENTICATED_FULLY is a special case: if specified, it must be satisfied in addition to the other rules in the list.

The built-in rules are as follows:

  • IS_AUTHENTICATED_ANONYMOUSLY – anyone has access; no need for the user to log in
  • IS_AUTHENTICATED_REMEMBERED – only known users that have logged in or are remembered from a previous session are allowed access
  • IS_AUTHENTICATED_FULLY – users must log in to gain access, even if they checked "remember me" last time

The first two of these distinguish between known and unknown users, where known users are ones that have an entry in the 'user' database table. The last is typically applied in cases where the user is accessing particularly sensitive information, such as bank account or credit card data. After all, someone else could be accessing your application using the "remember me" cookie from the previous user.

You can also apply the annotation to the controller class itself, which results in all actions inheriting the rules defined by it. If an action has its own annotation, that overrides the class-level one. The annotation isn't just limited to a list of rules like this either: take a look at the user guide to see how to use expressions to provide greater control over the rules.

Static URL rules

If annotations aren't your thing, you can define access control rules via a static map in Config.groovy . If you like to keep your rules in one place, it's ideal. Here is how you would define Hubbub's rules using this mechanism:

import grails.plugins.springsecurity.SecurityConfigType
...
grails.plugins.springsecurity.securityConfigType = SecurityConfigType.InterceptUrlMap
grails.plugins.springsecurity.interceptUrlMap = [
    '/timeline':         ['ROLE_USER'],
    '/person/*':         ['IS_AUTHENTICATED_REMEMBERED'],
    '/post/followAjax':  ['ROLE_USER'],
    '/post/addPostAjax': ['ROLE_USER', 'IS_AUTHENTICATED_FULLY'],
    '/**':               ['IS_AUTHENTICATED_ANONYMOUSLY']
]
 

Notice how the most general rule comes last? That's because order is important: Spring Security iterates through the rules and applies the first one that matches the current URL . So if the '/**' rule came first, your application would effectively be unprotected since all URLs would be matched to it. Also notice that you have to explicitly tell the plugin to use the map via the grails.plugins.springsecurity.securityConfigType settings.

Dynamic request maps

Do you want to update URL rules at runtime without restarting the application? If that's the case, you'll probably want to use request maps, which are basically URL rules stored in the database. To enable this mechanism, add the following to Config.groovy :

import grails.plugins.springsecurity.SecurityConfigType
...
grails.plugins.springsecurity.securityConfigType = SecurityConfigType.Requestmap

 All you then have to do is create instances of the Requestmap domain class, for example in BootStrap.groovy :

new Requestmap(url: '/timeline', configAttribute: 'ROLE_USER').save()
new Requestmap(url: '/person/*', configAttribute: 'IS_AUTHENTICATED_REMEMBERED').save()
new Requestmap(url: '/post/followAjax', configAttribute: 'ROLE_USER').save()
new Requestmap(url: '/post/addPostAjax', configAttribute: 'ROLE_USER,IS_AUTHENTICATED_FULLY').save()
new Requestmap(url: '/**', configAttribute: 'IS_AUTHENTICATED_ANONYMOUSLY').save()
 

Of course, there is a performance cost to this approach since it involves the database, but it is minimised through the use of caching. Take a look at the user guide for more information on this. Also, you don't have to worry about the order of the rules in this case because the plugin picks the most specific URL pattern that matches the current URL.

Which of these approaches should you use? It depends on how your application is set up and how you think about access control. Annotations make sense where rules apply on a per-controller basis and controllers have distinct URLs. If you tend to group controllers under a single URL, like /admin/ or you simply like to keep all your rules in one place, then you're probably better off with the static rules defined in Config.groovy . The third mechanism, request maps, only make sense if you want to add, change, or remove rules at runtime. A classic example where you might want to do this is in a CMS application, where URLs themselves are defined dynamically.

Whichever approach you take, once the rules are implemented your application is protected. For example, if you try to access the /timeline page in Hubbub at this point, you will be redirected to the standard login page:

Great! But who are you going to log in as? How are users going to log out? Protecting your pages is only the first step. You also need to make sure that you have the relevant security data (users and roles) and a user interface that's security aware.

Next steps

With the access control in place, you need to look at the user experience. Do you really want users clicking on links that they don't have access to? What about those roles you are using in the access control? When do they get created? Let's answer those questions now.

Security data

Some applications only care whether a user is known or not and in such cases you don't need to worry about roles because the IS_AUTHENTICATED_* rules are sufficient. But if your application needs more control over who has access to what, you will need roles. These are typically defined early in the life of the application and correspond to unchanging reference data. That makes BootStrap the ideal place to create them. For Hubbub, we add 'user' and 'admin' roles like so:

import org.example.SecRole

class BootStrap {
    def init = {
        ...
        def userRole = SecRole.findByAuthority('ROLE_USER') ?: new SecRole(authority: 'ROLE_USER').save(failOnError: true)
        def adminRole = SecRole.findByAuthority('ROLE_ADMIN') ?: new SecRole(authority: 'ROLE_ADMIN').save(failOnError: true)
        ...
    }
}
 

Of course, if the data already exists we don't want to recreate it, hence why we use findByAuthority() .

Adding users is almost as straightforward, but there are a couple of requirements that you need to bear in mind. First, the generated 'user' domain class has an enabled property that is false by default. If you don't explicitly initialise it to true the corresponding user won't be able to log in. Second, passwords are rarely stored in the database as plain text, so you will need to encode them first using an appropriate digest algorithm.

Fortunately, the plugin provides a useful service to help here: SpringSecurityService . Let's say we want to create an 'admin' user in Hubbub's BootStrap . The code would look something like this:

import org.example.*

class BootStrap {
    def springSecurityService

    def init = {
        ...
        def adminUser = User.findByUsername('admin') ?: new User(
                username: 'admin',
                password: springSecurityService.encodePassword('admin'),
                enabled: true).save(failOnError: true)

        if (!adminUser.authorities.contains(adminRole)) {
            SecUserSecRole.create adminUser, adminRole
        }
        ...
    }
}
 

We simply inject the security service into BootStrap and then use its encodePassword() method to convert the plain text password to its hash. This approach works particularly well when you decide to change the digest algorithm you use, because the service will encode passwords using the same algorithm as the one used when comparing them for authentication. In other words, the above code stays the same no matter what algorithm is used.

Once the user is created, we check whether it has the 'admin' role and if it doesn't, we assign the role to the user. We do this by way of the generated SecUserSecRole class and its create() method.

With the security data in place, and the knowledge of how to create it on demand where necessary, it's time to make the user interface aware of authentication, users, and roles.

The user interface

There are two aspects of the UI I want to look at here: displaying information specific to the user and making sure that the user can only see what he's allowed to. The first of these boils down to one question: how do we get the 'user' domain instance for the currently logged in user? Consider Hubbub's timeline page, which displays all the posts of the people that the current user is following:

class PostController {
    def springSecurityService
    ...
    @Secured(['ROLE_USER'])
    def timeline = {
        def user = User.get(springSecurityService.principal.id)

        def posts = []
        if (user.following) {
            posts = Post.withCriteria {
                'in'("user", user.following)
                order("createdOn", "desc")
            }
        }
        [ posts: posts, postCount: posts.size() ]
    }
    ...
}
 

As you can see, all we need to do is inject the security service again and use it to get hold of the principal. Unless you have created a custom version of the UserDetailsService (don't worry if you haven't come across this before), the principal will be an instance of org.codehaus.groovy.grails.plugins.springsecurity.GrailsUser whose id property contains the ID of the corresponding 'user' domain instance.

One thing you need to be aware of: if the current user is authenticated anonymously, i.e. he hasn't logged in and isn't remembered, the principal property will return a string instead. So if an action can be accessed by an unauthenticated user, make sure you check the type of the principal before using it!

What about ensuring users can only see what they are supposed to? For that, the plugin provides a rich set of GSP tags in the sec namespace. Let's say we want to add a couple of navigation links to Hubbub, but we only want to display one of them when the user isn't logged in and the other only if the user has the ROLE_USER role:

<sec:ifNotLoggedIn>
  <g:link controller="login" action="auth">Login</g:link>
</sec:ifNotLoggedIn>
<sec:ifAllGranted roles="ROLE_USER">
  <g:link class="create" controller="post" action="timeline">My Timeline</g:link>
</sec:ifAllGranted>
 

The markup inside the <sec:if*> tags will only be rendered to the page if the condition is satisfied. The plugin provides several other similar tags that all behave in a consistent fashion. See the user guide for more information.

The above example also shows you how to create a link to the login page. Allowing the user to log out is similarly straightforward. Hubbub provides a side panel that displays amongst other things the name of the logged in user and a link to sign out:

<sec:username /> (<g:link controller="logout">sign out</g:link>)
 

Easy! The combination of these tags and the security service should be more than sufficient to integrate your user interface with Spring Security. Just remember to keep your user interface elements in sync with your access control rules: you don't want bits of UI visible that result in an "unauthorised user" error.

I've now covered all the basic elements of the Spring Security plugin, but there are still two features that will affect a large number of users: AJAX requests and custom login forms.

The last pieces of the puzzle

How many web applications don't use AJAX to some degree now? And how many really want to use the stock login form for their application? It's fine for internal use, but I wouldn't recommend it for anything that's customer facing. Let's start with AJAX.

Securing AJAX requests

Dynamic user interfaces based on AJAX bring a new set of problems to access control. It's very easy to deal with a standard request that requires authentication: simply redirect the user to the login page and then redirect them back to the target page if the authentication is successful. But such a redirect doesn't work well with AJAX. So what do you do?

The plugin gives you a way to deal with AJAX request differently to normal ones. When an AJAX request requires authentication, Spring Security redirects to the authAjax action in LoginController rather than auth . But wait, that's still a redirect right? Yes, but you can implement the authAjax to send an error status or render JSON – basically anything that the client Javascript code can handle.

Unfortunately, the LoginController provided by the plugin doesn't implement authAjax at this time, so you will have to do add it yourself:

import javax.servlet.http.HttpServletResponse

class LoginController {
    ...
    def authAjax = {
        response.sendError HttpServletResponse.SC_UNAUTHORIZED
    }
    ...
}

 This is a very simple implementation that returns a 401 HTTP status code. How do we deal with such a response? That depends on what you use to implement AJAX in the browser. The example Hubbub application uses adaptive AJAX tags, so I'll use that to demonstrate the kind of thing you can do. This is part of the GSP template that is used for posting new messages:

<g:form action="ajaxAdd">
    <g:textArea id='postContent' name="content" rows="3" cols="50" onkeydown="updateCounter()" /><br/>
    <g:submitToRemote value="Post"
                 url="[controller: 'post', action: 'addPostAjax']"
                 update="[success: 'firstPost']"
                 onSuccess="clearPost(e)"
                 onLoading="showSpinner(true)"
                 onComplete="showSpinner(false)"
                 on401="showLogin();"/>
</g:form>
 

As you can see, it has an on401 attribute that specifies a bit of Javascript that should be executed when the AJAX submission returns a 401 status code. That bit of Javascript can, for example, display a dynamic, client-side login form for the user to authenticate with. Hubbub uses the client-side code provided in the plugin's user guide to do just that.

Note Version 1.1 of the plugin will come with a default implementation of the authAjax action.

You can also customise the ajaxSuccess and ajaxDenied actions to send back whatever response you want. As you can see, the server-side AJAX handling is simple and easy to customise. The real work has to be done in the client code.

Custom login forms

It's no longer fashionable to dedicate an entire page to the login form. These days applications are more likely to have a content-rich home page with a discrete login form located somewhere on it, perhaps only made visible by some Javascript magic. It's easy enough to provide your own dedicated login page (simply edit the auth action in LoginController and its associated GSP view to your heart's content), but what about a login panel?

It's not as hard as you might think. First of all, you need to decide where users should be redirected to when authentication is required. As you've probably gathered, this is /login/auth by default. Changing that default is as easy as adding a setting to Config.groovy :

grails.plugins.springsecurity.auth.loginFormUrl = '/'

 This line tells the plugin to redirect to the home page whenever authentication is required. All you then need to do is add a login panel to the home page. Here's an example GSP form that might go in such a panel:

<form method="POST" action="${resource(file: 'j_spring_security_check')}">
  <table>
    <tr>
      <td>Username:</td><td><g:textField name="j_username"/></td>
    </tr>
    <tr>
      <td>Password:</td><td><input name="j_password" type="password"/></td>
    </tr>
    <tr>
      <td colspan="2"><g:submitButton name="login" value="Login"/></td>
    </tr>
    <tr>
      <td colspan="2">try "glen" or "peter" with "password"</td>
    </tr>
  </table>
</form>
 

The key points here are:

  1. the form must use the POST method;
  2. the form must be submitted to <context>/j_spring_security_check;
  3. the username field must have the name 'j_username';
  4. the password field must have the name 'j_password'; and
  5. any "remember me" field must have the name '_spring_security_remember_me'.

As long as these requirements are satisfied, the login form will work perfectly. Well, not quite perfectly. If an authentication attempt fails via your login form, you will find yourself redirected back to the old login page. Fortunately, this is quickly rectified by adding another configuration setting:

grails.plugins.springsecurity.failureHandler.defaultFailureUrl = '/'
 

And that's all you need for a fully functioning login form! There are plenty of other options available to fine tune the behaviour, but you now have the basics on which to build.

This article has really only scratched the surface of the Spring Security plugin. I haven't mentioned HTTP Basic and Digest Authentication, events, salted passwords and more. That doesn't even include the other plugins that provide extra features such as alternative authentication mechanisms and access control lists (ACLs). But what you have read so far will enable you to get a fully working access control system up and running in no time. You will then be able to extend and customise as the need arises, knowing that Spring Security has more features than you will probably ever need.

分享到:
评论
1 楼 mohaowen1989 2012-03-13  
亲 有中文版的么?在grails基础上的spring-security3的入门学习

相关推荐

    Beginning Spring Boot 2 Applications and Microservices with the Spring Framework

    provides a rich ecosystem of projects to address modern application needs, like security, simplified access to relational and NoSQL datastores, batch processing, integration with social networking ...

    Spring.Essentials.178398

    Understand how to secure your Spring Web and standalone applications using Spring Security declaratively and consistently Get to grips with the end-to-end development of an API-based modern SPA using ...

    Beginning Spring Boot 2

    provides a rich ecosystem of projects to address modern application needs, like security, simplified access to relational and NoSQL datastores, batch processing, integration with social networking ...

    Beginning-Spring-Boot-2.pdf

    provides a rich ecosystem of projects to address modern application needs, like security, simplified access to relational and NoSQL datastores, batch processing, integration with social networking ...

    spring技术手册.zip

    15_Spring.pdf可能是一个关于特定Spring技术主题的文档,如Spring Security或Spring Integration,但没有具体信息无法详细描述。通常,这样的文档会深入探讨Spring框架的一个特定部分,例如身份验证和授权机制,或者...

    Simplified swarm optimization algorithm with local search.txt

    - **Simplified swarm optimization algorithm with local search**:此标题表明该研究提出了一个简化版本的粒子群优化算法,并且结合了局部搜索策略来解决特定问题。这里的“简化”意味着在保持算法基本结构的同时...

    SD Simplified Specifications.rar

    Part1_NFC_Interface_Simplified_Addendum_Ver1.00.pdf Part1_Physical_Layer_Simplified_Specification_Ver8.00.pdf Part1_UHS-II_Simplified_Addendum_Ver1.02.pdf PartA1_ASSD_Extension_Simplified_...

    Part A1 Advanced Security SD Extension Simplified Specification Ver2.00 Final

    这些拥有附加安全系统的SD卡被称为高级安全SD卡(Advanced Security SD,简称ASSD)。此文档为高级安全SD卡的开发、设计和应用提供了明确的技术指导,旨在提高数据存储介质的安全性和可靠性。 #### 高级安全SD卡...

    xpdf-chinese-simplified

    《xpdf-chinese-simplified:增强SWFTools的中文支持》 在IT行业中,处理文本和图形文件格式是一项常见的任务,而SWFTools是一个国外开发的开源软件工具集,主要用于处理Adobe Flash(SWF)文件。然而,由于其最初...

    [Chinese Simplified] Getting Started with Adobe Illustrator for Beginners Tutorial [DownSub.com].srt

    [Chinese Simplified] Getting Started with Adobe Illustrator for Beginners Tutorial [DownSub.com].srt

    Simplified Chinese.rar

    标题中的"Simplified Chinese.rar"表明这是一个简体中文版本的压缩文件,通常用于包含特定语言资源或软件的国际化版本。在本例中,它可能包含了针对简体中文用户的软件界面、文档或其他相关材料。 描述中提到的...

    Simplified Chinese.ptl

    Simplified Chinese.ptl

    Chinese ​(Simplified)​ Language Pack

    "Chinese (Simplified) Language Pack" 是一个针对简体中文用户设计的语言包,主要用于软件或操作系统,目的是为了提供中文界面,使用户能够更方便地理解和使用产品。这个语言包包含了各种用户界面元素的翻译,例如...

    【MSDN原版】Windows 7 with SP1各版本下载

    Windows 7 Ultimate with Service Pack 1 (x86) - DVD (Chinese-Simplified) : 下载地址(以下下载地址请右击复制快捷方式到电驴或者迅雷下载): ed2k://|file|cn_windows_7_ultimate_with_sp1_x86_dvd_618763.iso|...

    SD Simplified Specification

    SD Simplified Specification V3.0 是最新的版本,对于深入理解SD卡的工作原理、硬件设计以及在系统中的应用至关重要。本篇将围绕这个规范的几个关键部分进行详细阐述。 首先,我们关注的是Part_1_Physical Layer_...

    xpdf-chinese-simplified.zip

    Xpdf是一款开源的PDF文档阅读和处理工具,专门针对中文环境进行了优化,名为"xpdf-chinese-simplified",其压缩包文件"xpdf-chinese-simplified.zip"包含了适用于简体中文用户的所有组件。这款软件在IT领域中具有...

    xpdf-chinese-simplified.rar

    标题中的"xpdf-chinese-simplified.rar"表明这是一个与处理中文PDF文档相关的压缩包,其中包含了XPDF工具的简体中文版本。XPDF是一款开源的PDF文档处理工具集,主要用于PDF文档的查看、转换和提取信息,尤其在处理非...

Global site tag (gtag.js) - Google Analytics