Shiro is elegantly integrated into SpringBoot as a starter

Most of the articles on the web are about integration under SpringMVC in the past. Many people don’t know that Shiro provides an official starter that can be easily integrated with SpringBoot. This article describes my three integration approaches: 1. Use annotations entirely; 2. Use the URL configuration. 3. The URL configuration and annotation are mixed. The URL configuration is responsible for authentication control, and the annotation is responsible for permission control. The three methods have their own advantages and disadvantages, and need to be used in actual application scenarios.

code

Talk is cheap, show you my code: elegant-shiro-boot This project is built using Gradle and has three sub-projects:

  • Demo1 uses only annotations for authentication and authorization
  • Demo2 shows that only URL configurations are used for authentication and authorization
  • Demo3 Shows the combination of the two methods. The URL configuration controls authentication, and the annotation configuration controls authorization.

How to integrate

Integrating Apache Shiro into Spring-boot Applications

The funny thing is, I went directly to the official website and couldn’t find the document on this page. Instead, I found it through Google.

The introduction of this document is also fairly simple. We just need to introduce shro-spring-boot-starter according to the documentation, and then inject a custom Realm into the Spring container. Shiro can use this Realm to know how to get user information for Authentication. How to obtain user role and permission information to process Authorization.

Ps: Authentication is a process of checking whether a user has logged in, and authorization is a process of checking whether a logged in user has access permission.

The integration process is as follows: 1. Introduce starter, my project is built with Gradle, and Maven also introduces corresponding dependencies:

Dependencies {/ / spring boot the starter of the compile 'org. Springframework. The boot: spring - the boot - starter - web' compile 'org.springframework.boot:spring-boot-starter-aop' compile 'org.springframework.boot:spring-boot-devtools' testCompile 'org.springframework.boot:spring-boot-starter-test' //shiro compile 'org, apache shiro: shiro - spring - the boot - web - the starter: 1.4.0'}Copy the code

2. Write custom Realms

User.java (see classes in the com.abc.entity package on Github for other RBAC models)

public class User { private Long uid; // User ID private String uname; Private String Nick; // Private String PWD; // Encrypted login password private String salt; Private Date created; // Create time private Date updated; Private roles <String> = new HashSet<>(); Private Set<String> perms = new HashSet<>(); //getters and setters... }Copy the code

UserService.java

@service public class UserService {@param uname * @return */ public User findUserByName(String) uname){ User user = new User(); user.setUname(uname); user.setNick(uname+"NICK"); user.setPwd("J/ms7qTJtqmysekuY8/v1TAS+VKqXdH5sB7ulXZOWho="); // The password in plain text is 123456 user.setsalt ("wxKYXuTPST5SG0jMQzVPsg=="); User.setuid (new Random().nextlong ()); // Assign a random id user.setCreated(new Date()); return user; }}Copy the code

RoleService.java

@service public class RoleService {/** * simulation returns all roles of the user based on the user ID query.  * SELECT r.rval FROM role r, user_role ur * WHERE r.rid = ur.role_id AND ur.user_id = #{userId} * @param uid * @return */ public Set<String> getRolesByUserId(Long uid){ Set<String> roles = new HashSet<>(); // three programming languages represent three roles: js programmer, Java programmer, c++ programmer roles.add("js"); roles.add("java"); roles.add("cpp"); return roles; }}Copy the code

PermService.java

@service public class PermService {/** * impersonates a query based on the user ID to return all permissions of the user.  * SELECT p.pval FROM perm p, role_perm rp, user_role ur * WHERE p.pid = rp.perm_id AND ur.role_id = rp.role_id * AND ur.user_id = #{userId} * @param uid * @return */ public Set<String> getPermsByUserId(Long uid){ Set<String> perms = new HashSet<>(); // Perms.add (" HTML :edit"); //c++ programmer permission perms.add("hardware:debug"); Add (" MVN :install"); perms.add("mvn:clean"); perms.add("mvn:test"); return perms; }}Copy the code

CustomRealm.java

/** * This class is a reference to JDBCRealm, which defines how to query user information, how to query user roles and permissions. */ Public class CustomRealm extends AuthorizingRealm {@autoWired private UserService UserService; @Autowired private RoleService roleService; @Autowired private PermService permService; // Tell Shiro how to verify the password against the password and salt values in the obtained user information {// Set CredentialsMatcher HashedCredentialsMatcher hashMatcher = new HashedCredentialsMatcher(); hashMatcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME); hashMatcher.setStoredCredentialsHexEncoded(false); hashMatcher.setHashIterations(1024); this.setCredentialsMatcher(hashMatcher); } // Define the logic of how to obtain the user's roles and permissions, @override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {//null usernames are invalid if (principals == null) { throw new AuthorizationException("PrincipalCollection method argument cannot be null."); } User user = (User) getAvailablePrincipal(principals); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); System.out.println(" Get role info: "+ user.getroles ()); System.out.println(" get permission: "+ user.getperms ()); info.setRoles(user.getRoles()); info.setStringPermissions(user.getPerms()); return info; } // Define the business logic for how to get user information, Log in to Shiro @override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken = (UsernamePasswordToken) token; String username = upToken.getUsername(); // Null username is invalid if (username == null) { throw new AccountException("Null usernames are not allowed by this realm."); } User userDB = userService.findUserByName(username); if (userDB == null) { throw new UnknownAccountException("No account found for admin [" + username + "]"); } / / query user roles and permissions endures SimpleAuthenticationInfo, such elsewhere. / / the SecurityUtils getSubject () getPrincipal () will be able to take out all of the user's information, Including roles and permissions Set < String > roles. = roleService getRolesByUserId (userDB. GetUid ()); Set<String> perms = permService.getPermsByUserId(userDB.getUid()); userDB.getRoles().addAll(roles); userDB.getPerms().addAll(perms); SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userDB, userDB.getPwd(), getName()); if (userDB.getSalt() ! = null) { info.setCredentialsSalt(ByteSource.Util.bytes(userDB.getSalt())); } return info; }}Copy the code

3. Use annotations or URL configurations to control authentication and authorization

Please refer to the examples on the official website:

/ / url configuration @ Bean public ShiroFilterChainDefinition ShiroFilterChainDefinition () {DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition(); // logged in users with the 'admin' role chainDefinition.addPathDefinition("/admin/**", "authc, roles[admin]"); // logged in users with the 'document:read' permission chainDefinition.addPathDefinition("/docs/**", "authc, perms[document:read]"); // all other paths require a logged in user chainDefinition.addPathDefinition("/**", "authc"); return chainDefinition; }Copy the code
RequiresPermissions("document:read") public void readDocument() {// RequiresPermissions("document:read") public void readDocument() {... }Copy the code

4. Solve the bug of Using Spring AOP with annotation configuration. If you use Shiro annotation configuration along with the introduction of Spring AOP starter, there is a strange problem that causes shiro annotation requests to not be mapped and the following configuration needs to be added:

@Bean public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator(); /** * setUsePrefix(false) is used to solve a strange bug. In the case of Spring AOP. * Adding shiro annotations such as @requiresRole to a method of a class annotated with @Controller causes the method to fail to map requests, resulting in a 404 return. * to join this configuration to solve this bug. * / defaultAdvisorAutoProxyCreator setUsePrefix (true); return defaultAdvisorAutoProxyCreator; }Copy the code

Idea 1: Use annotations to control authentication and authorization

The advantage of using annotations is that they are fine-grained in control and are ideal for resource-based permission control.

For resource-based permission control, I suggest reading this article:
The New RBAC: Resource-Based Access Control

It’s very easy to just use annotations. We just need to configure the url configuration so that the request path can be accessed anonymously:

// Code in shiroconfig.java:  @Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() { DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition(); // Since Demo1 uses annotations for access control, all request paths can anonymously access chain.addPathDefinition("/**", "anon"); // All paths are managed via annotations // But let's use the one above, it's a little bit easier to understand. // or allow basic authentication, but NOT require it. // chainDefinition.addPathDefinition("/**", "authcBasic[permissive]"); return chain; }Copy the code

Then use the class annotations provided by Shiro on the controller class to do the controls:

annotations function
@RequiresGuest Only tourists can visit
@RequiresAuthentication Login is required to access
@RequiresUser Logged-in users or Remember me users can access
@RequiresRoles Users who have logged in must have a specified role to access the system
@RequiresPermissions Users who have logged in must have specified permissions to access the system

Code example :(refer to github code demo1 for more details)

/** * created by CaiBaoHong at 2018/4/18 15:51<br> */ @restController @requestMapping ("/t1") public Class Test1Controller {// Since there is no @requiresAuthentication annotation on the TestController class, // No user login is required to invoke the interface. So hello() and a1() are anonymously accessible @getMapping ("/hello") public String hello() {return "hello spring boot"; } // Visitors can visit, this is a bit of a pit, visitors mean: Subject. GetPrincipal ()==null // So when the user is not logged in, subject.getPrincipal()==null, the interface is accessible // But after the user is logged in, subject.getPrincipal()! @requiresGuest @getMapping ("/guest") public String guest() {return "@requiresGuest "; } // Only logged-in users can access the interface. This annotation is stricter than @requiresuser. UnauthenticatedException @requiresAuthentication @getMapping ("/authn") Public String Authn () {return "@RequiresAuthentication"; } // Logged-in users or remember me users can access // If the user is not logged in or not a Remember Me user calls the interface, UnauthenticatedException @RequiresUser @GetMapping("/user") public String user() { return "@RequiresUser"; } // This interface can be accessed because the user information returned by the UserService simulation has this permission. If the user is not logged in, UnauthenticatedException @RequiresPermissions("mvn:install") @GetMapping("/mvnInstall") public String mvnInstall() { return "mvn:install"; } // This interface cannot be accessed because UserService does not have the MVN :build permission. UnauthenticatedException // If you are logged in but do not have this permission, UnauthorizedException @requirespermissions ("gradleBuild") @getMapping ("/gradleBuild") public String gradleBuild() { return "gradleBuild"; } // This interface can be accessed because UserService emulated the user information returned with this role // If the user is not logged in, UnauthenticatedException @RequiresRoles("js") @GetMapping("/js") public String js() { return "js programmer"; } // This interface is accessible because the user information returned by UserService simulation has this role. // If there is no login, UnauthenticatedException // If there is no login, but there is no role, RequiresRoles("python") @getMapping ("/python") public String Python () {return "python programmer"; }}Copy the code

Idea 2: Use ONLY URL configuration to control authentication and authorization

Shiro provides and several default filters that we can use to configure and control permissions on specified urls:

Configuration for Corresponding filter function
anon AnonymousFilter Specifies that the URL can be accessed anonymously
authc FormAuthenticationFilter Specifies that the URL requires a form login, which is obtained from the request by defaultusername,password.rememberMeIf the login fails, the system switches to the loginUrl path. We can also use this filter as the default login logic, but we usually write our own login logic in the controller, so we can customize the message returned if we write it.
authcBasic BasicHttpAuthenticationFilter Basic login is required to specify the URL
logout LogoutFilter Logout filter, configure the specified URL can realize the exit function, very convenient
noSessionCreation NoSessionCreationFilter Disabling session creation
perms PermissionsAuthorizationFilter You need to specify permission to access
port PortFilter You need to specify a port for access
rest HttpMethodPermissionFilter Convert the HTTP request method into the corresponding verb to construct a permission string, this feeling is not meaningful, interested in looking at the source code comments
roles RolesAuthorizationFilter You need to specify a role to access
ssl SslFilter An HTTPS request is required for access
user UserFilter You need to be logged in or Remember Me to access it

In the spring container use ShiroFilterChainDefinition to control all the url of the authentication and authorization. The advantage is that the configuration granularity is large and multiple controllers can be authenticated and authorized. Here is an example of github code demo2:

@Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() { DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition(); /** * Watch out for potholes here! The context-path I set in application.yml: / API /v1 * However, after the actual test, the filter path is the path under context-path, without adding the "/ API /v1" prefix */ // access control chain.addPathDefinition("/user/login", "anon"); // You can access chain.addPathDefinition("/page/401", "anon") anonymously; // You can access chain.addPathDefinition("/page/403", "anon") anonymously; // Chain.addPathDefinition ("/t4/hello", "anon") can be accessed anonymously; // Chain.addPathDefinition ("/t4/changePwd", "authc") can be accessed anonymously; AddPathDefinition ("/t4/user", "user"); // Logged-in or "remember me" users can access chain.addPathDefinition("/t4/mvnBuild", "authc,perms[MVN :install]"); // MVN :build permission chain.addPathDefinition("/t4/gradleBuild", "authc,perms[gradle:build]"); // Need gradle:build permission chain.addPathDefinition("/t4/js", "authc,roles[js]"); // Need js role chain.addPathDefinition("/t4/python", "authc,roles[python]"); // Shiro needs the python character // shiro to provide the logout filter, access the specified request, the login will be performed, the default jump path is "/", // Because application-shiro. Yml is configured with shiro:loginUrl: / page / 401, // The /user/login and /t1/js interfaces can be combined to test whether the/T4 /logout interface is valid chain.addPathDefinition("/t4/logout", "anon,logout"); AddPathDefinition ("/**", "authc"); return chain; }Copy the code

Idea 3: The URL configuration controls authentication and the annotation controls authorization

Personally, I’m a big fan of annotations. However, flexible combination of the two configuration modes is the best practice for different application scenarios. Using only annotations or using only URL configuration can lead to some tiring work.

Let me give two scenarios: Scenario 1 suppose I write the system background management system, and my Java background is a pure return JSON data background, will not do the work of page skipping. That our background management system is generally all interfaces need to log in to access. If I were using annotations only, I would add @requiresAuthentication to each Controller to declare that every method under each Controller requires a login to access it. This is a bit troublesome, and when you add Controller in the future, you still need to add this annotation, in case you forget to add it, it will be wrong. Chain.addpathdefinition (“/**”, “authc”);

Scenario 2 Suppose I write the foreground of the mall, and my Java background is a background that purely returns JSON data. However, under the same Controller, some of these interfaces can be accessed anonymously, some can be accessed only by login, and some can be accessed only by specific roles and permissions. If only the URL is configured, each URL needs to be configured. In addition, the configuration error is easy, and the granularity is hard to control.

So my idea is: use URL configuration control authentication to achieve coarse-grained control; Use annotations to control authorization for fine-grained control.

Here is the sample code (see Github code at Demo3 for details) : shiroconfig.java

@configuration public class ShiroConfig {// Inject custom realm, Tell Shiro how to get user information to do login or permission control @bean public Realm Realm() {return new CustomRealm(); } @Bean public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator(); /** * setUsePrefix(false) is used to solve a strange bug. In the case of Spring AOP. * Adding the @requiresRole annotation to the method of the @Controller annotated class causes the method to fail to map the request, resulting in a 404 return. * adding this configuration fixes this bug */ creator. SetUsePrefix (true); return creator; } /** * Authentication is used to determine which request paths require user login and which request paths do not. * Only authentication is done here, not permission control, because permissions are done with annotations. * @return */ @Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() { DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition(); // Which requests can anonymously access chain.addPathDefinition("/user/login", "anon"); chain.addPathDefinition("/page/401", "anon"); chain.addPathDefinition("/page/403", "anon"); chain.addPathDefinition("/t5/hello", "anon"); chain.addPathDefinition("/t5/guest", "anon"); AddPathDefinition ("/**", "authc"); return chain; }}Copy the code

PageController.java

@restController@requestMapping ("/page") public class PageController {// shiro. I'm going to throw an exception directly to GlobalExceptionHandler to return json information. // You can also use json here, but this will be the same as the JSON returned by GlobalExceptionHandler. @RequestMapping("/401") public Json page401() { throw new UnauthenticatedException(); } // shiro. UnauthorizedUrl maps here. Because Demo3 provides url authentication control, // does not perform permission access control, that is, if there is no roles[js],perms[MVN :install] permission access control in ShiroConfig, // does not jump to this directory. @RequestMapping("/403") public Json page403() { throw new UnauthorizedException(); } @RequestMapping("/index") public Json pageIndex() { return new Json("index",true,1,"index page",null); }}Copy the code

GlobalExceptionHandler.java

/** * Catch shiro's exception, return a JSON message to the foreground, the foreground according to this information to display the corresponding prompt, or do the page jump. */ @ControllerAdvice public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); Private static final String GUEST_ONLY = "Attempting to perform a guest-only operation"; @ExceptionHandler(ShiroException.class) @ResponseBody public Json handleShiroException(ShiroException e) { String eName = e.getClass().getSimpleName(); Log. error("shiro execution error: {}",eName); Return new Json(eName, false, Codes.shiro_err, "Authentication or authorization error ", null); } @ExceptionHandler(UnauthenticatedException.class) @ResponseBody public Json page401(UnauthenticatedException e) { String eMsg = e.getMessage(); If (StringUtils. StartsWithIgnoreCase (eMsg GUEST_ONLY)) {return new Json (" 401 ", false, Codes. UNAUTHEN, "only allow tourists to visit, if you are logged in, ", null). Data ("detail", LLDB etMessage()); }else{return new Json("401", false, codes.unauthen, "user not logged in ", null).data("detail", LLDB etMessage()); } } @ExceptionHandler(UnauthorizedException.class) @ResponseBody public Json page403() { return new Json("403", false, Codes.UNAUTHZ, "User has no access permission ", null); }}Copy the code

TestController.java

@restController@requestMapping ("/t5") public class Test5Controller { @getMapping ("/hello") Public String hello() {return "hello spring boot"; } // If this path is not configured in ShiroConfig for anonymous access, it is directly filtered by login. // If anonymous access is configured, access can be accessed without login. @requiresGuest @getMapping ("/guest") public String guest() {return "@requiresGuest "; } @RequiresAuthentication @GetMapping("/authn") public String authn() { return "@RequiresAuthentication"; } @RequiresUser @GetMapping("/user") public String user() { return "@RequiresUser"; } @RequiresPermissions("mvn:install") @GetMapping("/mvnInstall") public String mvnInstall() { return "mvn:install"; } @RequiresPermissions("gradleBuild") @GetMapping("/gradleBuild") public String gradleBuild() { return "gradleBuild"; } @RequiresRoles("js") @GetMapping("/js") public String js() { return "js programmer"; } @RequiresRoles("python") @GetMapping("/python") public String python() { return "python programmer"; }}Copy the code