The introduction

In some older projects, shiro may be used for authorization authentication and verification, while Shiro is based on cookie/session. In the current scenario of front-end and back-end development, front-end developers need to debug locally and back-end, which is bound to encounter cross-domain problems. Now, with the Google Browser update, cookies are banned across domains.

Therefore, based on this background, the shiro framework of the old project was reformed to support token authentication without affecting the old code.

Shiro basic mechanics

Before starting Shiro’s transformation, we need to understand the basic mechanics of Shiro. There are four main components involved:

  • AuthenticationTokenAbstract interface for authentication, which returns two pieces of information, the Principal and the Credentials.
  • CredentilsMatcherThe credential verification interface is actually the password validator
  • AuthenticatingFilterAuthentication filters, created when the request comes inAuthenticationTokenObject and perform the login operation
  • AuthorizingRealmAuthorization interface, where access to permission information (doGetAuthorizationInfo) and user information (doGetAuthenticationInfo)

The basic process is as follows:

Transformation ideas

  1. To create aAuthenticationTokenInterface implementation class, used to store our Token information
  2. To create aAuthenticatingFilterImplementation class, here we need to do three things
    • The current Ruequest obtains the token, thus creatingAuthenticationTokenobject
    • inonAccessDeniedMethod to verify the validity of the Token
    • Finally, the login operation is performed. The login operation actually uses the token to exchange user informationAuthorizingRealmthedoGetAuthenticationInfomethods
  3. To create aAuthorizingRealmThere are three main things to do here
    • rewritesupportsMethod to make support for our custom tokens
    • implementationdoGetAuthorizationInfoMethod, which returns the set of permissions for the user
    • implementationdoGetAuthenticationInfoMethod, here we obtain user information based on Token
  4. To create aCredentilsMatcherThe implementation class of the interface, where we do not verify the password, returns true. Because when you get the Token, it proves that the account password has been verified. Therefore, the account and password verification is performed at the service layer and the Token is created only after the verification succeeds

Code implementation

  1. AuthenticationTokenThe implementation of the class
public class ShiroToken implements AuthenticationToken {
    private String token;
    public ShiroToken(String token) {
        this.token = token;
    }
    @Override
    public Object getPrincipal(a) {
        return token;
    }
    @Override
    public Object getCredentials(a) {
        returntoken; }}Copy the code
  1. AuthenticatingFilterThe implementation of the class
@Slf4j
public class TokenFilter extends AuthenticatingFilter {

    private static final String X_TOKEN = "X-Token";

    private ITokenService tokenService = null;

    /** * Create Token and support custom Token */
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        String token = this.getToken((HttpServletRequest)servletRequest);
        if(ObjectUtils.isEmpty(token)){
            log.error("token is empty");
            return null;
        }
        return new ShiroToken(token);
    }

    /** * compatible across domains */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        return ((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name());
    }

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String token = this.getToken(request);
        if(ObjectUtils.isEmpty(token)){
            this.respUnLogin(request, response);
            return false;
        }

        // Verify the Token validity
        if(tokenService == null){
            tokenService = SpringContext.getBean(ITokenService.class);
        }

        if(! tokenService.check(token)){this.respUnLogin(request, response);
        }

        TokenRealm#doGetAuthenticationInfo = TokenRealm#doGetAuthenticationInfo
        return executeLogin(servletRequest, servletResponse);
    }

    private void respUnLogin(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("application/json; charset=utf-8");
        response.setHeader("Access-Control-Allow-Credentials"."true");
        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));

        Response resp = new Response(BusinessCodeEnum.USER_UN_LOGIN.getCode(), BusinessCodeEnum.USER_UN_LOGIN.getMsg());
        response.getWriter().print(JSONUtil.toJsonStr(resp));
    }

    /** * Get token * First get from header * If not, get * from parameter@param request request
     * @return token
     */
    private String getToken(HttpServletRequest request){
        String token = request.getHeader(X_TOKEN);
        if(ObjectUtils.isEmpty(token)){
            token = request.getParameter(X_TOKEN);
        }
        returntoken; }}Copy the code

It is important to note here that TokenFilter is the implementation class of Filter and is not managed in the Spring container, so it cannot be injected through annotations such as @Autowire. It can only be retrieved through constructors or in the Spring context when used. It is recommended that you get it in the context of Spring and inject the possible pit through the constructor.

  1. AuthorizingRealmThe implementation of the class
@Slf4j
public class TokenRealm extends AuthorizingRealm {

    @Autowired
    @Lazy
    private ITokenService tokenService;

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof ShiroToken;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        log.info("user do authorization: {}", principalCollection);
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        log.info("user do authentication: {}", authenticationToken);
        ShiroToken token = (ShiroToken)authenticationToken;
        UserInfo userInfo = tokenService.getUserInfo(token.getCredentials().toString());
        if(userInfo == null) {throw BusinessCodeEnum.TOKEN_INVALID.getException();
        }
        return newSimpleAuthenticationInfo(userInfo.getUsername(), userInfo.getPassword(), userInfo.getNickName()); }}Copy the code
  1. CredentilsMatcherThe implementation of the class
public class TokenCredentialsMatcher implements CredentialsMatcher {
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        return token instanceofShiroToken; }}Copy the code

The last

With the above simple modification, we can implement token-based authentication without changing other features of Shiro framework.

Full source address: gitee.com/anyin/shiro…

If you have any questions, add your personal wechat friends to discuss: DaydayCoupons or follow AnyinWechart