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