10

I'm pretty new to Java and Spring 3 (used primarily PHP the past 8 years). I've gotten spring security 3 to work with all the default userDetails and userDetailsService and I know I can access the logged in user's username in a controller by using:

Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName(); //get logged in username

But there are two problems I can't figure out:

  1. There are a lot of other user details I would like stored when a user logs in (such as DOB, gender, etc.) and to be accessible via the controllers later on. What do I need to do so that the userDetails object that is created contains my custom fields?

  2. I'm already calling "HttpSession session = request.getSession(true);" at the top of each of my methods in my controller. Is it possible to store the logged in user's userDetails in a session upon login so that I don't need to also call "Authentication auth = SecurityContextHolder.getContext().getAuthentication();" at the beginning of every method?

Security-applicationContext.xml:

<global-method-security secured-annotations="enabled"></global-method-security>     
<http auto-config='true' access-denied-page="/access-denied.html">
    <!-- NO RESTRICTIONS -->        
    <intercept-url pattern="/login.html" access="IS_AUTHENTICATED_ANONYMOUSLY" />
    <intercept-url pattern="/*.html" access="IS_AUTHENTICATED_ANONYMOUSLY"  /> 
    <!-- RESTRICTED PAGES -->
    <intercept-url pattern="/admin/*.html" access="ROLE_ADMIN" />
    <intercept-url pattern="/member/*.html" access="ROLE_ADMIN, ROLE_STAFF" />

    <form-login login-page="/login.html"
                login-processing-url="/loginProcess"
                authentication-failure-url="/login.html?login_error=1"
                default-target-url="/member/home.html" />
    <logout logout-success-url="/login.html"/>
</http>

<authentication-manager>
    <authentication-provider>
        <jdbc-user-service data-source-ref="dataSource" authorities-by-username-query="SELECT U.username, UR.authority, U.userid FROM users U, userroles UR WHERE U.username=? AND U.roleid=UR.roleid LIMIT 1" />
        <password-encoder hash="md5"/>
    </authentication-provider>
</authentication-manager>

login.jsp:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form"%>

<tiles:insertDefinition name="header" />
<tiles:insertDefinition name="menu" />
<tiles:insertDefinition name="prebody" />

<h1>Login</h1>

<c:if test="${not empty param.login_error}">
    <font color="red"><c:out value="${SPRING_SECURITY_LAST_EXCEPTION.message}"/>.<br /><br /></font>
</c:if>
<form name="f" action="<c:url value='/loginProcess'/>" method="POST">
    <table>
        <tr><td>User:</td><td><input type='text' name='j_username' value='<c:if test="${not empty param.login_error}"><c:out value="${SPRING_SECURITY_LAST_USERNAME}"/></c:if>' /></td></tr>
            <tr><td>Password:</td><td><input type='password' name='j_password' /></td></tr>
            <tr><td>&nbsp;</td><td><input type="checkbox" name="_spring_security_remember_me" /> Remember Me</td></tr>
            <tr><td>&nbsp;</td><td><input name="submit" type="submit" value="Login" /></td></tr>
        </table>
    </form>

<tiles:insertDefinition name="postbody" />
<tiles:insertDefinition name="footer" />
Felix
  • 610
  • 2
  • 9
  • 21

3 Answers3

20

There's an awful lot going on in this question. I'll try to address it in pieces...

Q#1: There are a couple possible approaches here.

Approach #1: If you have other attributes that you want to add to your UserDetails object, then you should provide your own alternate implementation of the UserDetails interface that includes those attributes along with corresponding getters and setters. This would require that you also provide your own alternate implementation of the UserDetailsService interface. This component would have to understand how to persist these additional attributes to the underlying datastore, or when reading from that datastore, would have to understand how to populate those additional attributes. You'd wire all of this up like so:

<beans:bean id="userDetailsService" class="com.example.MyCustomeUserDetailsService">
<!-- ... -->
</beans:bean>

<authentication-manager alias="authenticationManager">
    <authentication-provider ref="authenticationProvider"/>
</authentication-manager>

<beans:bean id="authenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
    <beans:property name="userDetailsService" ref="userDetailsService"/>
</beans:bean>

Approache #2: Like me, you may find (especially over the span of several iterations) that you're better served to keep domain-specific user/account details separate from Spring Security specific user/account details. This may or may not be the case for you. But if you can find any wisdom in this approach, then you'd stick with the setup you have currently and add an additional User/Account domain object, corresponding repository/DAO, etc. If you want to retrieve the domain-specific user/account, you can do so as follows:

User user = userDao.getByUsername(SecurityContextHolder.getContext().getAuthentication().getName());

Q#2: Spring Security automatically stores the UserDetails in the session (unless you've explicitly taken steps to override that behavior). So there's no need for you to do this yourself in each of your controller methods. The SecurityContextHolder object you've been dealing with is actually populated (by SS) with SecurityContext including the Authentication object, UserDetails, etc. at the beginning of every request. This context is cleared at the end of each request, but the data always remains in the session.

It's worth noting, however, that it's not really a great practice to be dealing with HttpServletRequest, HttpSession objects, etc. in a Spring MVC controller if you can avoid it. Spring almost always offers cleaner, more idiomatic means of achieving things without the need for doing so. The advantage to that would be that controller method signatures and logic cease to be dependent on things that are difficult to mock in a unit test (e.g. the HttpSession) and instead of dependent on your own domain objects (or stubs/mocks of those domain objects). This drastically increases the testability of your controllers... and thus increases the liklihood that you actually WILL test your controllers. :)

Hope this helps some.

Kent Rancourt
  • 1,555
  • 1
  • 11
  • 19
  • Thanks Kent, that was quite helpful! Sorry for the mess, first time coding Spring. I like approach #2, where would I place that line of code, at the beginning of each method?And I guess based on that question, if I go with that 2nd approach is it possible to store the user object so that I don't need to create a new user object and go to the db for the same data each time? – Felix May 15 '12 at 20:36
  • 1
    The answer is, "it depends." I do find that with approach #2, I very rarely need to access a user's account (from a domain perspective). Maybe I access it only for purposes of displaying or editing their "profile." If that's the case, I would just read through to the datastore for this information when it's needed. If, however, you need to frequently access attributes of the user (from a domain perspective; perhaps some attribute of the user object needs to appear in the header, and is therefore needed in every request), then you probably should either reconsider approach #1. – Kent Rancourt May 15 '12 at 22:22
  • 1
    Thanks Kent. I ended up doing a hybrid type solution. When I need the logged in user I run a method in a wrapper controller to obtain the data. The method checks to see if the session contains a user object, and if it does it checks if the object's username equals that of the securityContext username. If not (or if session user object is null) it gets the user data from the database and stores the object in session. Two extra if statement, but 1 less database call. – Felix May 16 '12 at 13:44
  • 1
    If I'm reading your last comment correctly, you're guarding against a condition where the SS user and the domain user in session do not match. Because BOTH are stored in session, access to both is keyed off a single session cookie. There should be no scenario in which these don't match. You can eliminate that redundant check. The exception to this is if you use a "pre-authentication" solution such as SiteMinder- which would bring its own cookies into the equation. Then it would be possible for the SiteMinder session to end whilst the web session remains active or vice versa. N/A in your case. – Kent Rancourt May 16 '12 at 18:19
  • Ahh gotcha, that simplifies things! Thanks Kent. – Felix May 17 '12 at 13:56
  • Hi Kent, I implemented approach #1 but I'm having trouble getting the custom details once a user is logged in. My userDetails implementation has fields like userID, how would I then access this property in subsequent controllers? For example Authentication auth = SecurityContextHolder.getContext().getAuthentication(); auth.getName(); gets the username, but getUserID doesn't work. – Felix May 22 '12 at 17:37
  • here's how to retrieve it from the context: http://stackoverflow.com/a/6162091/80286 also, a full working example is here: http://howtodoinjava.com/2013/04/16/custom-userdetailsservice-example-for-spring-3-security/ this works because FTA: "for custom user detail service usage, AuthenticationProvider authenticates the user simply by comparing the password submitted in a UsernamePasswordAuthenticationToken against the one loaded by the UserDetailsService." – dev Dec 05 '14 at 22:23
  • and a more elegant way of resolving from the current context: http://stackoverflow.com/questions/8764545/how-to-get-active-users-userdetails – dev Dec 05 '14 at 22:36
2

In my opinion, Custom UserDetails implementation is great but should only be used for immutable characteristics of your user.

Once your custom User object overrides UserDetails, it's not easily changed. You have to create a whole new authentication object with the modified details and cannot just stick the modified UserDetails object back into the security context.

In application that I'm building I've realized this and had to rearchitect it so that upon successful authentication details about the user that are changing with every request (but that I don't want to reload from the db on every page load) are going to need to be kept in the session separately, but still only accessible/changeable after an authentication check.

Trying to figure out if this WebArgumentResolver mentioned in https://stackoverflow.com/a/8769670/1411545 is a better solution for my situation.

Community
  • 1
  • 1
Marc Johnson
  • 75
  • 1
  • 12
1

Accessing the session directly is a bit messy, and can be error prone. For example, if the user is authenticated using remember-me or some other mechanism which doesn't involve a redirect, the session won't be populated until after that request completes.

I would use a custom accessor interface to wrap the calls to the SecurityContextHolder. See my answer to this related question.

Community
  • 1
  • 1
Shaun the Sheep
  • 22,353
  • 1
  • 72
  • 100