Skip to content
JackSparrow414
Go back

Using Shiro: A Basic Login Flow

Table of contents

Open Table of contents

Article body

If I am going to do this, I might as well do it thoroughly!

Configuration before using Shiro:

  1. Add the shiro-spring integration dependency to the POM.

  2. Configure the Shiro filter in web.xml to intercept all requests.

<filter>
        <filter-name>shiroFilter</filter-name>
        <filter-class>
           org.springframework.web.filter.DelegatingFilterProxy
        </filter-class>
        <init-param>
            <!-- This parameter delegates Shiro lifecycle management to Spring -->
            <param-name>targetFilterLifecycle</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>shiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <!-- REQUEST is the default even when omitted -->
        <!-- This element accepts four values: REQUEST, FORWARD, INCLUDE, and ERROR.
        Any number of <dispatcher> elements may be added within <filter-mapping>, so the filter applies to requests
        coming directly from clients, through forward, through include, or
        through <error-page>. -->
        <dispatcher>REQUEST</dispatcher>
    </filter-mapping>
  1. Write shiro.xml. Its main configuration currently includes:
<bean id="shiroFilter" class="org.apache.shiro.spring.web.YOUR_SHIRO_FACTORY_BEAN">
   <!-- Required securityManager property: the environment in which Shiro operates -->
   <property name="securityManager" ref="securityManager"/>
   <!-- Login URL -->
   <property name="loginUrl" value="portal/login"/>
   <!-- URL for authenticated users who lack the required permission -->
   <property name="unauthorizedUrl" value="/403"/>
   <!-- authc requires authentication; anon does not -->
   <property name="filterChainDefinitions">
      <value>
         /protal/index = authc
         /portal/login = anon
         /403 = anon
         /portal/show = roles["admin"]
         /** = authc
      </value>
   </property>
</bean>

<!-- Reference securityManager -->
<bean id="securityManager"class="org.apache.shiro.web.mgt.DefaultWebSecurityManage>
  <!-- Custom realm: extend Shiro and override methods for custom authentication and authorization -->
  <property name="customRealm" ref="customRealm"/>
</bean>
  1. Import shiro.xml in the Spring configuration:

  2. Implement a custom Realm extending AuthorizingRealm. Consider these situations in a real login flow:

First: the user is not registered or does not exist. Throw UnKnownAccountException and indicate that the account does not exist or is unregistered.

Second: the username and password do not match. Throw IncorrectCredentialsException and report an incorrect username or password, without specifically identifying the password as incorrect, for security reasons.

Third: after too many incorrect password attempts, reject login requests for a period of time. I have not implemented this yet and will add it later.

Fourth: if repeated login -> rejection -> login -> rejection cycles occur in a short time, lock the account for security reasons. Throw LockedAccountException, tell the user the account is locked, and require a more secure account-recovery process.

P.S. The third and fourth measures help prevent brute-force attacks and reduce server pressure from many requests in a short period.

 @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 1. Obtain the user information from authenticationToken and cast it to UsernamePasswordToken
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;

        // 2. Obtain username from UsernamePasswordToken and query the database by username
        String username = token.getUsername();

        // 3. Set properties on the entity model
        // Why not fetch username and query whether it matches the current password?
        // I find querying with an entity more convenient: the database returns a match or no match; the alternative would be cumbersome
        User authUser = new User();

        authUser.setUserName(username);
        // The database query in the real project returns a User object
        authUser = getUserByInfo(authUser);

        // Check the returned User object
        if (authUser == null){
            throw new UnknownAccountException("用户名不存在");
        }
        if ("SD".equals(authUser.getState())){
            throw new LockedAccountException("账号被锁定,请联系管理员!");
        }
        // Crucial: info argument 2 must be the stored password returned by the database, not token.getPassWord()
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(authUser,authUser.getPassWord(),getName());

        return info;
    }

April 20, 2019 update: be very careful that info argument 2 is the password value retrieved from the database, not token.getPassWord(). The correct form is:
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(authUser,authUser.getPassWord(),getName());

The following is an incorrect example.

You might previously have obtained the password submitted at login:

String password = String.valueOf(token.getPassWord());

You might then write the following, which produces a null-pointer or credentials-mismatch error during authentication:

SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(authUser,password,getName());

The reason is explained at the end of this article.

With the basic configuration complete, begin the login flow. Here is a simple implementation:

@requestMapping(value = "loginAuth")
public String loginAuth(String username, String password) {

        // 1. Obtain the current subject: the key to entering the Shiro system
        Subject subject = SecurityUtils.getSubject();
        // 2. Information to authenticate: what we present to Shiro
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
        // 3. Authenticate in the custom realm
            subject.login(token);
            return "redirect:/portal/index";
        } catch (Exception e) {
            System.out.println(e.getMessage());
           return "redirect:/portal/login";
        }

    }

subject.login(token) invokes authentication in the custom Realm. After successful authentication, the user enters the system.

Important notes:

  1. Here Shiro is integrated with Spring MVC, Spring, and MyBatis. Your application is probably also using Shiro within the Spring family. For loginUrl in the Shiro configuration, 90% of online posts tell you to use the login page’s address in the web project and say Shiro looks for a page named login by default. Remember: if you integrate with Spring MVC, its url-pattern in web.xml is likely /, *.jsp, *.html, *.do, or similar. Do not blindly set loginUrl to the page name. In my experience, that fails to redirect 90% of the time and produces a “No request Mapping with…” error. Since Spring MVC intercepts the requests, configure the URL that routes to the login page, rather than the page’s actual name. Shiro intercepts requested URLs, not page names, despite how many posts describe this.

  2. You do not actually need successUrl here; configuring it does not help, and the online explanations are fairly reliable on this point. After authentication, Shiro returns to the previous request URL by default. My first login handler returned void, so Shiro returned my request URL, Spring MVC intercepted it, tried to locate the corresponding view (which did not exist), and failed. Online solutions uniformly suggested overriding the relevant Shiro method and setting a custom URL. I wondered why everyone would go to that trouble just for a redirect. On my second attempt, I used ModelAndView to select the view directly, avoiding the override. However, although the view changed, the URL did not: Shiro still returned the previous URL. That is a serious problem. If the new page performs business operations, requests usually use ${baseUrl}/specific-controller/specific-method, where baseUrl derives from the current URL. That URL is wrong because it never changed. Spring MVC may fail to find the controller, or may find one whose business logic is not what we intended. On my third attempt, I used redirect immediately after authentication. This both navigates to the correct page and updates the URL.

Two different application scenarios:

  1. E-commerce platforms such as Taobao and Tmall do not require login simply to enter the site. They prompt for login only when an operation must be associated with a user, such as adding to a cart or saving a favorite. Entry is allowed without interception (anon). When the user triggers an operation—accessing an authc URL—Shiro routes them to the configured loginUrl (the specific action/controller). After authentication, it returns them with their user information to the previous page to complete the operation.

  2. An administration system typically verifies the user as soon as they access it. Intercept the initial request, then allow the authenticated user to enter the home page or navigation menu.

This explanation is somewhat long, but I hope it helps you understand what you are using rather than copying online posts, leaving holes everywhere, and becoming unsure how it works.

Do not let ambition outpace hands-on practice!


Share this post:

Continue this series

Using Shiro

  1. Using Shiro: A Basic Login FlowYou are here
  2. Using Shiro Remember Me and Automatic Login: Fixing a deleteMe Cookie
  3. Using Shiro: Basic Session Management
  4. Using Shiro: Password Hashing and Salting, with Authentication Troubleshooting
  5. Using Shiro: A Primer on Tokens and Why to Use Them
  6. Using Shiro with Tokens
  7. Using Shiro: Integrating JWT for More Capable Tokens
  8. Using Shiro: Complete Spring Boot Integration Code

Comments

Questions, corrections, and experiences are welcome. Sign in with GitHub to comment; both language versions share this discussion.

Comments are available on the live site only.