Shiro+Token under Springboot uses redis as a security authentication scheme

In the previous project, permission authentication did not use a security framework, and it was judged in the custom filter whether to log in and whether the user has operation permission.
Recently, a new project was opened. When building a shelf, I thought of using a security framework to solve the authentication problem. spring security is too large, and our project is not large, so we decided to use Shiro.

What is Shiro

Apache Shiro is a powerful and flexible open source security framework that fully handles authentication, authorization, encryption and session management.

Realm is the core component of Shiro, and it is also a two-step process, authentication and authorization. The performance in Realm is the following two methods.

  • Authentication: doGetAuthenticationInfo, the core function is to judge whether the login information is correct
  • Authorization: doGetAuthorizationInfo, the core function is to obtain the user's authorization string for subsequent judgment

Shiro filter

When Shiro is applied to a web project, Shiro will automatically create some default filters to filter client requests. Here are some of the filters provided by Shiro:

filter describe
anon Indicates that it can be used anonymously
authc Indicates that authentication (login) is required to use
authcBasic Indicates httpBasic authentication
perms When there are multiple parameters, each parameter must be passed before passing perms["user:add:"]
port port[8081] jumps to schema://serverName:8081?queryString
rest permission
roles Role
ssl Indicates a secure url request
user Indicates that the user must exist, and does not check when logging in

Why choose shiro

  • Simplicity, Shiro is simpler to use and easier to understand than Spring Security.
  • Flexibility, Shiro can run in any application environment such as Web, EJB, IoC, Google App Engine, etc. without relying on these environments. And Spring Security can only be integrated with Spring.
  • Pluggable, Shiro's clean API and design patterns make it easy to integrate with many other frameworks and applications. Shiro integrates seamlessly with third-party frameworks such as Spring, Grails, Wicket, Tapestry, Mule, Apache Camel, Vaadin. Spring Security seems to be a little tricky in this regard.

spring boot integration shiro

add maven dependencies

Introducing shiro into the project is very simple, we just need to introduce shiro-pring and that's it

<!-- SECURITY begin -->
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-spring</artifactId>
  <version>1.4.0</version>
</dependency>
<!-- SECURITY end -->

shiro custom authentication token

AuthenticationToken is used to collect identities (such as usernames) and credentials (such as passwords) submitted by users. Shiro will call the doCredentialsMatch method of the CredentialsMatcher object to match the AuthenticationInfo object with the AuthenticationToken. If the match is successful, it means that the subject (Subject) authentication is successful, otherwise, it means that the authentication fails.

Shiro only provides a UsernamePasswordToken that can be used directly to implement authentication based on username/password subject (Subject). UsernamePasswordToken implements RememberMeAuthenticationToken and HostAuthenticationToken, which can support "remember me" and "host authentication".

Our business logic is to call the interface every time, instead of using the session to store the login status, we use the method of storing the token in the head, so we do not use the session and do not require user password authentication.

The custom token is as follows:

/**
 * Created by Youdmeng on 2020/6/24 0024.
 */
public class YtoooToken implements AuthenticationToken {
    private String token;
    public YtoooToken(String token) {
        this.token = token;
    }
    @Override
    public Object getPrincipal() {
        return token;
    }
    @Override
    public Object getCredentials() {
        return token;
    }
}

shiro custom Realm

Realm is the core component of shiro and mainly handles two functions:

  • Authentication We receive the token passed by the filter and authenticate the token of the login operation
  • Authorization Obtain the login user information, and obtain the user's permissions and store them in roles, so as to verify the operation permissions of the interface later
@Slf4j
public class UserRealm extends AuthorizingRealm {
    @Autowired
    private JedisClusterClient jedis;
    /**
     * Big pit! , this method must be rewritten, otherwise Shiro will report an error
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof YtoooToken;
    }
     /**
     * Authorize
     *
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("Shiro Rights Profile");
        String token = principals.toString();

        UserDetailVO userDetailVO = JSON.parseObject(jedis.get(token), UserDetailVO.class);

        Set<String> roles = new HashSet<>();
        roles.add(userDetailVO.getAuthType() + "");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setRoles(roles);
        return info;
    }
    /**
     * Certification
     *
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        log.info("Shiror Certification");
        YtoooToken usToken = (YtoooToken) token;
        //Get the user's entered account.
        String sid = (String) usToken.getCredentials();
        if (StringUtils.isBlank(sid)) {
            return null;
        }
        log.info("sid: " + sid);
        return new SimpleAccount(sid, sid, "userRealm");
    }
}

shiro custom interceptor

Customize the shiro interceptor to control the access rights of the specified request, and log in to shiro for authentication

Our custom shiro interceptor mainly uses two of these methods:

  • isAccessAllowed() Determines whether it is possible to log in to the system
  • onAccessDenied() When isAccessAllowed() returns false, the login is denied, enter this interface for exception handling
/**
 * Created by Youdmeng on 2020/6/24 0024.
 */
@Slf4j
public class TokenFilter extends FormAuthenticationFilter {
    private String errorCode;
    private String errorMsg;
    private static JedisClusterClient jedis = JedisClusterClient.getInstance();
    /**
     * If false is returned here, request onAccessDenied()
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String sid = httpServletRequest.getHeader("sid");
        if (StringUtils.isBlank(sid)) {
            this.errorCode = ResponseEnum.TOKEN_UNAVAILABLE.getCode();
            this.errorMsg = ResponseEnum.TOKEN_UNAVAILABLE.getMessage();
            return false;
        }
        log.info("sid: " + sid);
        UserDetailVO userInfo = null;
        try {
            userInfo = JSON.parseObject(jedis.get(sid), UserDetailVO.class);
        } catch (Exception e) {
            this.errorCode = ResponseEnum.TOKEN_EXPIRE.getCode();
            this.errorMsg = ResponseEnum.TOKEN_EXPIRE.getMessage();
            return false;
        }
        if (userInfo == null) {
            this.errorCode = ResponseEnum.TOKEN_EXPIRE.getCode();
            this.errorMsg = ResponseEnum.TOKEN_EXPIRE.getMessage();
            return false;
        }
        //refresh timeout
        jedis.expire(sid, 30 * 60); //30 minutes expired
        YtoooToken token = new YtoooToken(sid);
        // Submit to realm for login, if there is an error, he will throw an exception and be caught
        getSubject(request, response).login(token);
        // If no exception is thrown, it means the login is successful and returns true
        return true;
    }
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) {
        ResponseMessage result = Result.error(this.errorCode,this.errorMsg);
        String reponseJson = (new Gson()).toJson(result);
        response.setContentType("application/json; charset=utf-8");
        response.setCharacterEncoding("utf-8");
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            outputStream.write(reponseJson.getBytes());
        } catch (IOException e) {
            log.error("Permission check exception",e);
        } finally {
            if (outputStream != null){
                try {
                    outputStream.flush();
                    outputStream.close();
                } catch (IOException e) {
                    log.error("permission check,close connection exception",e);
                }
            }
        }
        return false;
    }
}

Configure ShiroConfig

In springboot, components are managed by spring through @Bean, where securityManager, shiroFilter, AuthorizationAttributeSourceAdvisor need to be configured

inject realm

@Bean
public UserRealm userRealm() {
    UserRealm userRealm = new UserRealm();
    return userRealm;
}

inject securityManager

@Bean("securityManager")
public DefaultWebSecurityManager getManager(UserRealm realm) {
    DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
    // Use your own realm
    manager.setRealm(realm);
    /*
      * Close the session that comes with shiro, see the documentation for details
      * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
      */
    DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
    DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
    defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
    subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
    manager.setSubjectDAO(subjectDAO);

    return manager;
}

inject shiroFilter

Here, add custom filters to shiro, and configure which paths to execute, and those filtering rules for shiro

@Bean("shiroFilter")
public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
    ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

    // Add your own filter and name it token
    Map<String, Filter> filterMap = new HashMap<>();
    filterMap.put("token", new TokenFilter());
    factoryBean.setFilters(filterMap);

    factoryBean.setSecurityManager(securityManager);
    /*
      * custom url rules
      * http://shiro.apache.org/web.html#urls-
      */
    Map<String, String> filterRuleMap = new HashMap<>();

    //swagger
    filterRuleMap.put("/swagger-ui.html", "anon");
    filterRuleMap.put("/**/*.js", "anon");
    filterRuleMap.put("/**/*.png", "anon");
    filterRuleMap.put("/**/*.ico", "anon");
    filterRuleMap.put("/**/*.css", "anon");
    filterRuleMap.put("/**/ui/**", "anon");
    filterRuleMap.put("/**/swagger-resources/**", "anon");
    filterRuleMap.put("/**/api-docs/**", "anon");
    //swagger
    //Log in
    filterRuleMap.put("/login/login", "anon");
    filterRuleMap.put("/login/verifyCode", "anon");
    // All requests go through our own JWT Filter
    filterRuleMap.put("/**", "token");
    factoryBean.setFilterChainDefinitionMap(filterRuleMap);
    return factoryBean;

Configure DefaultAdvisorAutoProxyCreator

Solution Adding shiro annotations such as @RequiresRole to the method of the @Controller annotated class will cause the method to fail to map the request, resulting in a 404 return.

@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
    DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
    /**
      * setUsePrefix(false)Used to fix a weird bug. In the case of introducing spring aop.
      * Adding shiro annotations such as @RequiresRole to the method of the @Controller annotated class will cause the method to fail to map the request, resulting in a 404 return.
      * Adding this configuration can solve this bug
      */
    defaultAdvisorAutoProxyCreator.setUsePrefix(true);
    return defaultAdvisorAutoProxyCreator;
}

Configure AuthorizationAttributeSourceAdvisor to make doGetAuthorizationInfo() Shiro permission configuration take effect

@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
    AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
    authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
    return authorizationAttributeSourceAdvisor;
} 

Control permissions in the interface

Use the RequiresRoles annotation to configure the permissions required by the interface

When configuring logical = Logical.OR, the permission to log in to this configuration is any of 1, 2, and 3, which can successfully access the interface

@ApiOperation("task scheduling")
@PostMapping("/dispatch")
@RequiresRoles(value = { "1", "2", "3" }, logical = Logical.OR)
public ResponseMessage dispatch(@RequestBody @Valid DispatchVO dispatchVO) {

    log.info("Task scheduling starts Input parameters:" + JSON.toJSONString(dispatchVO));
    try {
        service.dispatch(dispatchVO);
        return Result.success(ResponseEnum.SUCCESS.getCode(), ResponseEnum.SUCCESS.getMessage());
    } catch (RuntimeException e) {
        log.error("Task scheduling failed", e);
        return Result.error(ResponseEnum.ERROR.getCode(), e.getMessage());
    } catch (Exception e) {
        log.error("Task scheduling failed", e);
        return Result.error(ResponseEnum.ERROR.getCode(), ResponseEnum.ERROR.getMessage());
    }
}

Unified exception handling

Configure global exception handling

@ControllerAdvice
@Order(value=1)
public class ShiroExceptionAdvice {

    private static final Logger logger = LoggerFactory.getLogger(ShiroExceptionAdvice.class);
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler({AuthenticationException.class, UnknownAccountException.class,
            UnauthenticatedException.class, IncorrectCredentialsException.class})
    @ResponseBody
    public ResponseMessage unauthorized(Exception exception) {
        logger.warn(exception.getMessage(), exception);
        logger.info("catch UnknownAccountException");
        return Result.error(ResponseEnum.NOT_AUTHORIZED.getCode(), ResponseEnum.NOT_AUTHORIZED.getMessage());
    }

    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(UnauthorizedException.class)
    @ResponseBody
    public ResponseMessage unauthorized1(UnauthorizedException exception) {
        logger.warn(exception.getMessage(), exception);
        return Result.error(ResponseEnum.NOT_AUTHORIZED.getCode(), ResponseEnum.NOT_AUTHORIZED.getMessage());
    }
}

The redis tool used above

@Bean
    @DependsOn("ConfigUtil")
    public JedisClusterClient getClient() {

        ml.ytooo.redis.RedisProperties.expireSeconds = redisProperties.getExpireSeconds();
        ml.ytooo.redis.RedisProperties.clusterNodes = redisProperties.getClusterNodes();
        ml.ytooo.redis.RedisProperties.connectionTimeout = redisProperties.getConnectionTimeout();
        ml.ytooo.redis.RedisProperties.soTimeout = redisProperties.getSoTimeout();
        ml.ytooo.redis.RedisProperties.maxAttempts = redisProperties.getMaxAttempts();

        if (StringUtils.isNotBlank(redisProperties.password)) {
            ml.ytooo.redis.RedisProperties.password = redisProperties.password;
        }else {
            ml.ytooo.redis.RedisProperties.password = null;
        }

        return JedisClusterClient.getInstance();
    }
@Data
@Component
@ConfigurationProperties(prefix = "redis.cache")
public class RedisProperties {

    private int expireSeconds;
    private String clusterNodes;
    private int  connectionTimeout;
    private String password;
    private int soTimeout;
    private int maxAttempts;
}

Dependency toolset:

<dependency>
  <groupId>ml.ytooo</groupId>
  <artifactId>ytooo-util</artifactId>
  <version>3.7.0</version>
</dependency>

knock off





For more interesting and beautiful content, welcome to my blog to communicate and make progress together         WaterMin


Tags: Spring Boot Shiro

Posted by Harry57 on Wed, 01 Jun 2022 02:23:01 +0530