Write in front
The appearance of this article means that the research on the implementation of Spring Security Oauth is no longer carried out. The reason is that the original open source project has been abandoned and no longer updated. Moreover, the content of Oauth implementation is a little strange and new spring-authorization-server It has only been released to 0.1.0. By default, it only provides memory based implementation. Personally, I think it is not very perfect and is not suitable for use in the project. Moreover, the Oauth process of Spring Security has been implemented. If you want to modify it, you have to study the implementation logic of spring authorization server again, and then modify and customize it. It's too energy-consuming. It's better to use Shiro to implement the logic of Oauth.
However, this article has nothing to do with Oauth. It is simply the integration of Shiro + Springboot + JWT.
The dependencies used in this article are as follows:
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.7.1</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.11.0</version> </dependency>
Another thing to note is that Shiro has a default configuration for SpringBoot. For details, see the official website:
- Integration with Spring: http://shiro.apache.org/spring-framework.html
- Integration with SpringBoot: http://shiro.apache.org/spring-boot.html
However, in this article, the SpringBoot integration method is not used, and the default configuration is not imported according to the tutorials on the official website, because Shiro needs to be configured relatively simply to run. Moreover, from my understanding, Shiro is suitable for the mode of login state storage based on Session, and the state is stored in Token using Jwt. This mode needs to make some changes to the logic of configuration, In addition, there are some pits, so the configuration is not based on the default configuration. The points needing attention will be explained below.
The record post has just contacted Shiro. If there is any error in the content, I hope the boss can correct it.
1. Implementation source code
The sample project has been uploaded to Github and is directly available: https://github.com/nineya/framework-study/tree/master/shiro-study
Project structure:
1.1 SpringBoot program entry
package com.nineya.shiro; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @author Farewell to shangxue * 2021/2/15 * Program entry */ @SpringBootApplication public class ShiroApplication { public static void main(String[] args) { SpringApplication.run(ShiroApplication.class); } }
1.2 entity class
Simply customize user, role and permission entity classes. A user can contain multiple roles and a role can contain multiple permissions.
package com.nineya.shiro.entity; /** * @author Farewell to shangxue * 2021/2/15 * jurisdiction */ public class Permissions { private long id; private String permissionsName; public Permissions() { } public Permissions(long id, String permissionsName) { this.id = id; this.permissionsName = permissionsName; } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getPermissionsName() { return permissionsName; } public void setPermissionsName(String permissionsName) { this.permissionsName = permissionsName; } @Override public String toString() { final StringBuilder sb = new StringBuilder("Permissions{"); sb.append("id=").append(id); sb.append(", permissionsName='").append(permissionsName).append('\''); sb.append('}'); return sb.toString(); } }
package com.nineya.shiro.entity; import java.util.Set; /** * @author Farewell to shangxue * 2021/2/15 * Roles, including permission sets */ public class Role { private long id; private String roleName; /** * The set of permissions that a role has */ private Set<Permissions> permissions; public Role() { } public Role(long id, String roleName, Set<Permissions> permissions) { this.id = id; this.roleName = roleName; this.permissions = permissions; } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getRoleName() { return roleName; } public void setRoleName(String roleName) { this.roleName = roleName; } public Set<Permissions> getPermissions() { return permissions; } public void setPermissions(Set<Permissions> permissions) { this.permissions = permissions; } @Override public String toString() { final StringBuilder sb = new StringBuilder("Role{"); sb.append("id=").append(id); sb.append(", roleName='").append(roleName).append('\''); sb.append(", permissions=").append(permissions); sb.append('}'); return sb.toString(); } }
package com.nineya.shiro.entity; import java.util.Set; /** * @author Farewell to shangxue * 2021/2/15 * Users, including role sets */ public class User { private long uid; private String userName; private String password; /** * User's corresponding role */ private Set<Role> roles; public User() { } public User(long uid, String userName, String password, Set<Role> roles) { this.uid = uid; this.userName = userName; this.password = password; this.roles = roles; } public long getUid() { return uid; } public void setUid(long uid) { this.uid = uid; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Set<Role> getRoles() { return roles; } public void setRoles(Set<Role> roles) { this.roles = roles; } @Override public String toString() { final StringBuilder sb = new StringBuilder("User{"); sb.append("uid=").append(uid); sb.append(", userName='").append(userName).append('\''); sb.append(", password='").append(password).append('\''); sb.append(", roles=").append(roles); sb.append('}'); return sb.toString(); } }
1.3 service layer
Create a Login service layer implementation, which can access and read user information for user Login operation. In actual use, it is modified as; Connect to the database to get data.
package com.nineya.shiro.service; import com.nineya.shiro.entity.User; /** * @author Farewell to shangxue * 2021/2/15 * User login service interface */ public interface LoginService { /** * Get users by user name * * @param name user name * @return */ User getUserByName(String name); }
package com.nineya.shiro.service.impl; import com.nineya.shiro.entity.Permissions; import com.nineya.shiro.entity.Role; import com.nineya.shiro.entity.User; import com.nineya.shiro.service.LoginService; import org.springframework.stereotype.Service; import java.util.*; /** * @author Farewell to shangxue * 2021/2/15 * Users log in to the service and store user information in the format of map < < user name, user information > for query * In actual use, you can modify this to connect to the database query */ @Service public class LoginServiceImpl implements LoginService { private final Map<String, User> users = new HashMap<>(); public LoginServiceImpl() { // Define three permissions Permissions permissions1 = new Permissions(1, "create"); Permissions permissions2 = new Permissions(2, "delete"); Permissions permissions3 = new Permissions(3, "select"); // Define two roles Role role1 = new Role(1, "read", new HashSet<Permissions>(){{add(permissions3);}}); Role role2 = new Role(1, "write", new HashSet<Permissions>(){{add(permissions1);add(permissions2);}}); // Define three users corresponding to two roles users.put("observe", new User(1, "observe", "123456", Collections.singleton(role1))); users.put("admin", new User(1, "admin", "123456", Collections.singleton(role2))); users.put("user", new User(1, "user", "123456", new HashSet<Role>(){{add(role1); add(role2);}})); } @Override public User getUserByName(String name) { return users.get(name); } }
1.4 Token tools
The token tool class is responsible for the creation and parsing of JWT. In this paper, username is added to the load. In practical use, role and other information can be added together. However, a large amount of data and implicit data should not be added to JWT, because JWT is not encrypted. A large amount of data will lead to too long token and increase the network load.
package com.nineya.shiro.util; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.TokenExpiredException; import com.auth0.jwt.interfaces.DecodedJWT; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.Date; /** * jwt Processing class * * @author Farewell to shangxue * 2020/11/29 */ @Component public class UserTokenUtil { /** * jwt encryption algorithm */ private Algorithm algorithm; private JWTVerifier verifier; public UserTokenUtil() { algorithm = Algorithm.HMAC256("secret"); verifier = JWT.require(algorithm).build(); } /** * Create a user token and save the creation time of the token * * @param username User name * @return token character string */ public String createToken(String username) { Date expireTime = new Date(System.currentTimeMillis() + 60 * 60 * 1000); return JWT.create() .withClaim("username", username) .withExpiresAt(expireTime) .sign(algorithm); } /** * Verify the legitimacy of the token * * @param token * @return */ public DecodedJWT verifyToken(String token) { try { return verifier.verify(token); } catch (Exception e) { throw new TokenExpiredException("token Parsing failed"); } } /** * Get user name * * @param token * @return */ public String getUserName(String token) { DecodedJWT jwt = verifyToken(token); return jwt.getClaim("username").asString(); } }
1.5 controller (start with Shiro)
Add the login login interface implementation, and add several simple interfaces as subsequent call examples. Here we begin to design different problems of jwt logic described above.
package com.nineya.shiro.controller; import com.nineya.shiro.entity.User; import com.nineya.shiro.service.LoginService; import com.nineya.shiro.util.UserTokenUtil; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresRoles; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.Date; /** * @author Farewell to shangxue * 2021/2/15 */ @RestController public class LoginController { @Resource private LoginService loginService; @Resource private UserTokenUtil tokenUtil; /** * When logging in with jwt, the logic here will be somewhat different * If Token is not used, the user will pass subject. In this method Login (usernamepasswordtoken). * When using jwt, session will no longer be used to store login status, subject The login (usernamepasswordtoken) logic will be performed when the Filter parses the token, and * Each request requires token parsing and login operations. * That is to say, there are two steps: authentication and authorization. Originally, you only need to authenticate when logging in and authorize each request. After using jwt, each request requires three steps: memory jwt parsing, authentication and authorization. * @param userName user name * @param password password * @return */ @GetMapping("/login") public String login(@RequestParam("userName") String userName, @RequestParam("password") String password) { if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(password)) { return "Please enter user name and password!"; } User user = loginService.getUserByName(userName); if (!user.getPassword().equals(password)) { return "Incorrect password!"; } return tokenUtil.createToken(userName); } // This is a session based implementation without jwt // @GetMapping("/login") // public String login(User user) { // if (StringUtils.isEmpty(user.getUserName()) || StringUtils.isEmpty(user.getPassword())) { // return "please enter your user name and password!"; // } // //User authentication information // Subject subject = SecurityUtils.getSubject(); // UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken( // user.getUserName(), // user.getPassword() // ); // try { // //For verification, you can catch the exception and return the corresponding information // subject.login(usernamePasswordToken); subject.checkRole("admin"); subject.checkPermissions("query", "add"); // } catch (UnknownAccountException e) { // return "the user name does not exist!"; // } catch (AuthenticationException e) { // return "wrong account or password!"; // } catch (AuthorizationException e) { // return "no permission"; // } // return "login success"; // } /** * Allow users whose role is read and write to access * @return */ @RequiresRoles({"read", "write"}) @GetMapping("/admin") public String admin() { return "admin"; } /** * Allow users with select permission to access * @return */ @RequiresPermissions("select") @GetMapping("/select") public String select() { return "select"; } /** * Allow users with create permission to access * @return */ @RequiresPermissions("create") @GetMapping("/create") public String create() { return "create"; } }
package com.nineya.shiro.controller; import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.subject.Subject; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; /** * @author Farewell to shangxue * 2021/2/15 * Handle some exceptions that fail to pass authority authentication */ @ControllerAdvice public class ExceptionController { @ExceptionHandler @ResponseBody public String ErrorHandler(AuthorizationException e) { return "Failed permission verification!\n" + e.getMessage(); } }
1.6 custom JWT filter
Check whether the request header has the Authorization field. If so, perform token parsing and login operations.
package com.nineya.shiro.filter; import com.nineya.shiro.controller.ExceptionController; import org.apache.shiro.authc.BearerToken; import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; /** * @author Farewell to shangxue * 2021/2/15 */ public class TokenFilter extends BasicHttpAuthenticationFilter { /** * Determine whether the user wants to log in. * Check whether the header contains the Authorization field */ @Override protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) { HttpServletRequest req = (HttpServletRequest) request; String authorization = req.getHeader(AUTHORIZATION_HEADER); return authorization != null; } /** * Here we will explain in detail why the final return is true, that is, access is allowed * For example, we provide an address GET /article * Login users and visitors see different content * If false is returned here, the request will be directly intercepted and the user can't see anything * So we return true here. In the Controller, we can use subject Isauthenticated() to determine whether the user logs in * If some resources can only be accessed by logged in users, we only need to add the @ requireauthentication annotation on the method * However, one disadvantage of this is that it is not able to filter and authenticate GET,POST and other requests separately (because we have rewritten the official method), but it has little impact on the application */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { //Judge whether the request header of the request carries "Token" if (isLoginAttempt(request, response)) { //If it exists, enter the executeLogin method to perform login and check whether the token is correct try { return executeLogin(request, response); } catch (Exception e) { //token error e.printStackTrace(); return false; } } //If there is no token in the request header, it may be to perform login operation or visitor status access. There is no need to check the token and return true directly return true; } /** * User login * @param request * @param response * @return */ @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String token = httpServletRequest.getHeader(AUTHORIZATION_HEADER); BearerToken jwtToken = new BearerToken(token, request.getRemoteAddr()); getSubject(request, response).login(jwtToken); return true; } }
1.7 custom Realm
Shiro provides the implementation of JdbcRealm, IniRealm and DefaultLdapRealm by default, but it does not meet the scenario I need, nor can it realize the resolution of jwt. Therefore, Shiro adopts to inherit the authorizing Realm custom Realm class and replicate the two interfaces of doGetAuthorizationInfo (authorization) and doGetAuthenticationInfo (authentication).
In fact, there are pits here, and the logic is not very suitable for the implementation of jwt, but there is no good scheme to modify, which will be described in detail later.
-
This is just an example. The loginservice is authorized and authenticated twice The getuserbyname (name) operation can be avoided.
-
The JWT can be parsed as an intermediate object in the authentication step, and then the intermediate object can be used as the Principal, which can avoid the performance waste caused by repeated parsing of JWT.
package com.nineya.shiro.config; import com.nineya.shiro.entity.Permissions; import com.nineya.shiro.entity.Role; import com.nineya.shiro.entity.User; import com.nineya.shiro.service.LoginService; import com.nineya.shiro.util.UserTokenUtil; import org.apache.catalina.realm.AuthenticatedUserRealm; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthenticatingRealm; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.util.StringUtils; import javax.annotation.Resource; import java.util.stream.Collectors; /** * Custom realm * * @author Farewell to shangxue * 2021/2/15 */ public class StudyRealm extends AuthorizingRealm { @Resource private LoginService loginService; @Resource private UserTokenUtil tokenUtil; @Override public boolean supports(AuthenticationToken token) { return token instanceof BearerToken; } /** * Authorization, executed after authentication * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String token = (String) principals.getPrimaryPrincipal(); String name = tokenUtil.getUserName(token); User user = loginService.getUserByName(name); // Add roles and permissions SimpleAuthorizationInfo simpleAuthenticationInfo = new SimpleAuthorizationInfo(); for (Role role : user.getRoles()) { // Add role simpleAuthenticationInfo.addRole(role.getRoleName()); // add permission simpleAuthenticationInfo.addStringPermissions(role.getPermissions().stream() .map(Permissions::getPermissionsName).collect(Collectors.toSet())); } return simpleAuthenticationInfo; } /** * authentication * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { if (StringUtils.isEmpty(token.getPrincipal())) { return null; } String name = tokenUtil.getUserName((String) token.getPrincipal()); User user = loginService.getUserByName(name); if (user == null) { return null; } // The first parameter is the principal, which will be encapsulated as principalcollection at the time of authorization Getprimaryprincipal() is used, so the jwt content must be returned // The second parameter is the authentication information, that is, the password. For the subsequent verification to pass, it needs to be the same as the content in the token // The third parameter is the domain name return new SimpleAuthenticationInfo(token.getPrincipal(), token.getCredentials(), user.getUserName()); } }
1.8 Shiro configuration
package com.nineya.shiro.config; import com.nineya.shiro.filter.TokenFilter; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.realm.Realm; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; import java.util.HashMap; import java.util.Map; /** * @author Farewell to shangxue * 2021/2/15 * Configuration class */ @Configuration public class ShiroConfiguration { /** * If the agent is not configured, the annotation will not take effect * @return */ @Bean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator(); defaultAAP.setProxyTargetClass(true); return defaultAAP; } /** * If the agent is not configured, the annotation will not take effect * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * Add your own authentication method to the container * @return */ @Bean public Realm studyRealm() { StudyRealm studyRealm = new StudyRealm(); return studyRealm; } /** * The implementation of the Filter should not be registered as a bean, otherwise the Filter sequence will be disordered and an exception will be thrown * If you must register as a Bean, you can use Order to specify the priority, which has not been tried yet * @return */ public TokenFilter tokenFilter() { return new TokenFilter(); } /** * Filter Factory, set the corresponding filter conditions and jump conditions * @return */ @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager()); Map<String, String> map = new HashMap<>(); //Log out map.put("/logout", "logout"); // Use the jwt filter name we created ourselves map.put("/**", "jwt"); //Sign in shiroFilterFactoryBean.setLoginUrl("/login"); //home page shiroFilterFactoryBean.setSuccessUrl("/select"); //Error page, authentication failed, jump shiroFilterFactoryBean.setUnauthorizedUrl("/error"); shiroFilterFactoryBean.setFilterChainDefinitionMap(map); shiroFilterFactoryBean.setFilters(new HashMap<String, Filter>(){{put("jwt", tokenFilter());}}); return shiroFilterFactoryBean; } /** * Permission management, configuration is mainly the management and authentication of Realm, and cache management can be configured at the same time * @return */ @Bean public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager webSecurityManager = new DefaultWebSecurityManager(); //realm management webSecurityManager.setRealm(studyRealm()); return webSecurityManager; } }
2. Why does Shiro not support JWT very well?
At the beginning of this article, Shiro's support for Jwt mode is not very good. It's all my personal understanding. If the content is wrong, please leave a message and ask questions.
2.1 authentication / authorization mode BUG
- Impact on Performance
Shiro as a whole revolves around the authentication / authorization mode, which is applicable to the system with status (i.e. the server stores login information). Authentication is carried out during login. Once login is completed, authentication steps are no longer required, and authentication is carried out directly according to the permission configuration. JWT is a stateless implementation. All login information is stored in the token. In the filter, getsubject (request, response) needs to be executed according to the token information every time Login (jwttoken) method, and then go through the doGetAuthenticationInfo method of realm for authentication.
For JWT, this authorization and authentication step can actually be combined as an authorization step. Login account password verification is the authentication step, but getsubject (request, response) cannot be called during login Login (jwttoken) to log in, you must call the login according to the token in the filter, which leads to a waste of performance. You can parse the JWT as an intermediate object in the authentication step, and then take the intermediate object as the Principal, which can avoid the performance waste caused by repeatedly parsing the JWT, but the performance waste caused by Shiro's repeated authentication process is inevitable.
- cache hidden BUG
Because the Token of JWT is time-effective, and the Token is used as the certificate of the authentication step, if the cache is enabled, the cache will be stored according to the Token, so that the subsequent authentication process will not be carried out for the Token, and the invalid Token can also access the system.
The disgusting thing is that the public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) method cannot replicate, so we can only turn off the caching function to solve this problem, or implement these contents manually. If the cache is turned off, the authorization step is the same cache logic, and the cache of the authorization step will also be turned off, which will also affect the performance.
To sum up, Shiro's support for JWT is not very good, but it is not impossible to solve it. It's good to manually copy these classes, but it will be cumbersome.