Search code examples
javasecurityjpashiro

Web application Apache Shiro integration


I'm trying to understand the Apache Shiro workflow and how to integrate it into my application. What I can't understand is how and where do I perform the login and then send the redirect? Or does Shiro do this automatically (because I specified the realm in the ini file)? Can I send custom information (user attributes) along with the redirect (via Servlet response rather than backing bean)?

What I understand and have so far:

Add the Shiro Listener and Filter to the web.xml file so it can respond to requests:

<listener>
    <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>

<filter>
    <filter-name>ShiroFilter</filter-name>
    <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>ShiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
    <dispatcher>INCLUDE</dispatcher>
    <dispatcher>ERROR</dispatcher>
</filter-mapping>

Create a shiro.ini file where you can configure some attributes for Shiro:

[main]
shiro.loginURL = /login.xhtml
myRealm = com.example.shiro.MyRealm
securityManager.realms = $myRealm

[urls]
/account/** = authc
/logout = logout

Create the custom realm class:

@Stateless
@Local(Realm.class)
public class SecurityBean implements Realm{
    private EntityManager em;
    private TypedQuery<Credential> cQuery;

    @Override
    public Account getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        try{
            cQuery = em.createNamedQuery("getCredentials", Credential.class);
            cQuery.setParameter("userName", ((UsernamePasswordToken) token).getUsername());
            Credential c = cQuery.getSingleResult();
            boolean b = Encryption.compare(new String(((UsernamePasswordToken) token).getPassword()), c.getSalt(), c.getEncryptedPassword());
            if(b){
                return c;
            }else{
                throw new AuthenticationException("Passwords do not match.");
            }
        }catch(Exception e){
            throw new AuthenticationException("Error verifying credentials.");
        }
    }

    @Override
    public String getName() {
        return "User Realm";
    }

    @Override
    public boolean supports(AuthenticationToken token) {
        if(token instanceof UsernamePasswordToken){
            return true;
        }else{
            return false;
        }
    }
}

Where Encryption is a class to encrypt and compare passwords and Credential is a JPA Entity class to store the username and password:

@NamedQueries({@NamedQuery(name = "getCredentials", query = "SELECT c FROM Credential c WHERE c.userName = :userName"),
            @NamedQuery(name = "deleteCredentials", query = "DELETE FROM Credential c WHERE c.userName = :userName")})
@Entity
public class Credential implements Serializable, Account{
    private static final long serialVersionUID = 2555682746921997545L;

    @Id @GeneratedValue
    private long id;

    private String userName;

    private String encryptedPassword;
    private String salt;

    private User user;

    //... getters/setters...
}

Where User is a JPA Entity class that actual stores a user's application information.


Solution

  • Shiro will automatically intercept any request to a restriced resource and will display the defined login screen. You only need to declare what is the authentication level that your app resources required. Taking your code:

    [main]
    shiro.loginURL = /login.xhtml
    myRealm = com.example.shiro.MyRealm
    securityManager.realms = $myRealm
    
    [urls]
    /account/** = authc
    /logout = logout
    

    You are telling Shiro that:

    1. the login screen to be shown to users is /login.xhtml
    2. your Realm class is com.example.shiro.MyRealm. Shiro will instantiate it and it will delegate app-specific security operations to it. Here is where you can write your specific security code. If I do not understand wrongly, your SecurityBean is your custom Realm, and thus it either should be renamed to com.example.shiro.MyRealm or you change the myRealm to point to that class.
    3. /account/** = authc: every resource within /account/ requires the user to be authenticated. Thus, any attempt to go there will cause Shiro to display the login page if the user is not already authenticated.
    4. When the user goes to /logout, shiro will end the session

    What I see is that you are missing this line at the very beginning of the [urls] section: /login.xhtml = authc. The /login.xhtml is not restricted to authenticated users (otherwise no one could log in), but the authc filter must still be specified for it so it can process that url's login submissions. It is 'smart' enough to allow those requests through as specified by the shiro.loginUrl above.

    BTW, if you also want to have a complete user management solution take a look a Stormpath. We also have a plugin that seamlessly works with Shiro. You can take a look at our sample Stormpath Shiro app

    Disclaimer, I am an active Stormpath contributor.