reference resources: Shiro Springboot cluster shared Session (Redis) + single user login
https://zhuanlan.zhihu.com/p/54176956
Frame construction
1. Basic environment
jdk8
maven
lombok
spring boot 2.5.7
2. Import shiro maven coordinates
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-web-starter</artifactId> <version>1.8.0</version> </dependency>
3. Create a new custom Realm class to realize the core logic of authentication and authentication
Create userinfo java:
@Setter @Getter public class UserInfo implements Serializable { private String username; private String password; private Set<String> roles; private Set<String> perms; }
Create customrealm java:
import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import java.util.HashSet; public class CustomRealm extends AuthorizingRealm { /** * identity authentication * The main function is to provide an identity authentication function. The basic idea is to find the user's identity information from the database and give it to the Shiro framework. The Shiro framework will automatically compare it with the account information transmitted from the login page to see if it matches. If it matches, the login is successful, otherwise the login fails * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //Log in to TOKEN, including the user account and password UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken; String username = upToken.getUsername(); //The following multiple judgments can be added or deleted according to the business // Judge whether the user name does not exist, and throw an exception if it does not exist if (username == null) { throw new AccountException("Null usernames are not allowed by this realm."); } //Analog data, you can obtain the current user information by looking up the database UserInfo user = new UserInfo(); user.setUsername("aesop"); user.setPassword("123"); //The roles and permissions of the query user are saved in SimpleAuthenticationInfo, which can be used elsewhere //SecurityUtils.getSubject().getPrincipal() can take out all the user's information, including roles and permissions /** Save User permissions and roles into the User object*/ HashSet<String> roles = new HashSet<>(); roles.add("admin"); roles.add("teacher"); user.setRoles(roles); HashSet<String> perms = new HashSet<>(); perms.add("blog:read"); perms.add("blog:search"); user.setPerms(perms); //You can also save additional information to the Session //SecurityUtils.getSubject().getSession().setAttribute(Constants.SESSION_USER_INFO, userInfo); //Construction verification information return SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), getName()); return info; } /** * to grant authorization * After the identification is completed, the permission is given to the current user for detailed control according to the permission in the future * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { //null usernames are invalid if (principals == null) { throw new AuthorizationException("PrincipalCollection method argument cannot be null."); } //Gets the User object corresponding to the current User UserInfo user = (UserInfo) getAvailablePrincipal(principals); //Create permission object SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); //Set user role (user.getRoles () is a set < string >, [admin,student...]) info.setRoles(user.getRoles()); //Set user license (user.getPerms() is a set < string >, [blog:read,blog:search...]) info.setStringPermissions(user.getPerms()); return info; } }
3. Add Shiro interception configuration
Create shiroconfig java:
package com.example.springshirodemo.config.shiro; import org.apache.shiro.session.mgt.eis.SessionDAO; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.RedisTemplate; import javax.servlet.Filter; import java.util.HashMap; import java.util.Map; /** * Shiro Core configuration */ @Configuration public class ShiroConfig { /** * shiro Unified authority determination * Intercept or release permissions according to business needs. anon: all requests can be accessed, authc: access can only be accessed after login and authentication * @return */ @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager(myRealm())); Map<String, Filter> filters = new HashMap<>(); filters.put("authc", new LoginFormFilter()); shiroFilterFactoryBean.setFilters(filters); Map<String, String> map = new HashMap<>(); // Log in and log out map.put("/doLogin", "anon"); map.put("/logout", "logout"); // swagger map.put("/swagger**/**", "anon"); map.put("/webjars/**", "anon"); map.put("/v2/**", "anon"); // Authenticate all users map.put("/**", "authc"); // Not logged in, redirect path // shiroFilterFactoryBean.setLoginUrl("/login"); // home page // shiroFilterFactoryBean.setSuccessUrl("/index"); // Error page, authentication failed, jump // shiroFilterFactoryBean.setUnauthorizedUrl("/error"); shiroFilterFactoryBean.setFilterChainDefinitionMap(map); return shiroFilterFactoryBean; } /** * Inject custom CustomRealm into SecurityManager * @return */ @Bean public CustomRealm myRealm() { return new CustomRealm(); } /** * Inject custom CustomRealm into SecurityManager * Shiro Manage internal component instances through SecurityManager, and provide various security management services through it * @return */ @Bean public DefaultWebSecurityManager securityManager(CustomRealm customRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // Custom Realm securityManager.setRealm(customRealm); return securityManager; } }
When the user-defined login fails or there is no login, the json format is returned instead of redirecting to login JSP page. Note: after this is configured, the redirect path configuration setLoginUrl will fail
Create ShiroLoginFilter class:
import cn.aesop.common.restful.ResultBean; import cn.aesop.common.restful.ResultCode; import com.alibaba.fastjson.JSON; import org.apache.shiro.web.filter.authc.FormAuthenticationFilter; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletResponse; import java.io.PrintWriter; /** * @author: hxy * @description: Intercept the requests without login and return all json information Overwrite shiro's original jump login Interception method of JSP * @date: 2017/10/24 10:11 */ public class ShiroLoginFilter extends FormAuthenticationFilter { @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) { PrintWriter out = null; HttpServletResponse res = (HttpServletResponse) response; try { res.setCharacterEncoding("UTF-8"); res.setContentType("application/json"); out = response.getWriter(); out.println(JSON.toJSONString(ResultBean.FAIL(ResultCode.E_201))); } catch (Exception e) { } finally { if (null != out) { out.flush(); out.close(); } } return false; } }
4. Annotation authority
Add the following comments on the Controller interface to intercept requests without permission
@RequiresRoles(value={"admin","user"},logical = Logical.OR) @RequiresPermissions(value={"add","update"},logical = Logical.AND)
If multiple permissions / roles are verified, they are separated by "," in the middle. By default, all listed permissions / roles must be met at the same time before they take effect. However, there is logical = logical in the annotation Or this one. You can make permission control more flexible here.
If OR is set here, it means that one of the listed conditions can be met. If it is not written OR set to logical = logical And means that all listed must be met before entering the method.
I didn't have a deep understanding of the method of using subject to control through code, so I didn't find such permission control. In addition, the use of annotations is more concise and clear, so individuals prefer to use annotation to control.
So far, a basic shrio + spring boot framework has been built
5. Get context information
After successful login, you can obtain the currently logged in user information through the following code
Subject currentUser = SecurityUtils.getSubject(); UserInfo principal = (UserInfo)currentUser.getPrincipal(); //Or get customized information from the session //Session session = SecurityUtils.getSubject().getSession(); //UserInfo principal = (UserInfo) session.getAttribute(Constants.SESSION_USER_INFO);
5. Password encryption
The above example password is directly stored in plaintext in the database, which is not secure. It can only be stored after encryption, and it should form a system with identity authentication. The basic modification steps are described below:
1) Create voucher matcher
/** * Voucher matcher * (Because our password verification was handed over to Shiro's SimpleAuthenticationInfo for processing * Therefore, we need to modify the code in doGetAuthenticationInfo; * ) * The voucher matcher can be extended to realize the functions of locking after entering the number of password errors, and the next time */ @Bean(name = "credentialsMatcher") public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); //Hash algorithm: MD5 algorithm is used here; hashedCredentialsMatcher.setHashAlgorithmName("md5"); //The number of hashes, such as hashing twice, is equivalent to md5(md5("")); hashedCredentialsMatcher.setHashIterations(2); //storedCredentialsHexEncoded is true by default. In this case, the password is used, and the Hex code is used for encryption; Base64 encoding is used when false hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true); return hashedCredentialsMatcher; }
2) Set the voucher matcher at the injection CustomRealm. The modification code is as follows
/** * Inject custom permission verification object * Shiro Realm The user-defined Realm inherited from the authoringrealm, that is, the class that Shiro authenticates the user to log in is user-defined */ @Bean public CustomerRealm userRealm() { CustomerRealm realm = new CustomerRealm(); realm.setCredentialsMatcher(hashedCredentialsMatcher()); return realm; }
3) Modify the doGetAuthenticationInfo method of the CustomerRealm class
... //Add salt=username+salt SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(username+"salt"), getName()); ...
4) When registering a user or creating a password, use the following rules to create an encrypted password and store it in the database
// md5 + salt + hash hash times Md5Hash md5Hash2 = new Md5Hash(password, username+"salt", 2); return md5Hash2.toString();
reference resources: shiro uses Md5 encryption
6. session persistence and distributed session sharing
Save sessions to redis, and use the same redis for multi machine deployment to ensure that sessions are shared with each other; The system restarts and the user does not need to log in again
1)maven pom joins redis
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
2)application.yml configuration
spring: redis: host: localhost #redis service PI port: 6379 #Server
Basic operation of Redis
@Autowired private RedisTemplate<String, Object> redisTemplate; //preservation redisTemplate.opsForValue().set("key-1", "value-1"); //Preservation with expiration date redisTemplate.opsForValue().set("key-1", "value-1", 120, TimeUnit.SECONDS); //delete redisTemplate.delete("key-1");
3) Create a class, inherit cacheingsessiondao, and customize the session persistence implementation
The four methods to Override are:
doCreate: when shiro creates a session, it saves the session to redis
doUpdate: the effective time to refresh the session when the user maintains the session
doDelete: deletes the session from redis when the user logs off or the session expires
doReadSession: shiro obtains the Session object from sessionId and redis
Create redissessiondao java
import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.eis.CachingSessionDAO; import org.springframework.data.redis.core.RedisTemplate; import java.io.Serializable; import java.util.concurrent.TimeUnit; /** * Custom session persistence implementation, Shiro extension for cluster sharing */ public class RedisSessionDAO extends CachingSessionDAO { //The prefix of the SessionID stored in Redis private static final String PREFIX = "SENTGON_SHOP_SHIRO_SESSION_ID"; //Validity period (time unit will be increased in subsequent use, in seconds) private static final int EXPRIE = 86400; //1 day //Redis operation tool private RedisTemplate<Serializable, Session> redisTemplate; //Constructor public RedisSessionDAO(RedisTemplate<Serializable, Session> redisTemplate) { this.redisTemplate = redisTemplate; } /** * shiro When creating a session, save the session to redis * @param session * @return */ @Override protected Serializable doCreate(Session session) { //Generate SessionID Serializable serializable = this.generateSessionId(session); assignSessionId(session, serializable); //Store the sessionid as the Key and the session as the value in redis redisTemplate.opsForValue().set(PREFIX+serializable, session); return serializable; } /** * When the user maintains the session, refresh the effective time of the session * @param session */ @Override protected void doUpdate(Session session) { //Set session validity session.setTimeout(EXPRIE * 1000); //Store the sessionid as the Key and the session as the value in redis, and set the validity period redisTemplate.opsForValue().set(PREFIX+session.getId(), session, EXPRIE, TimeUnit.SECONDS); } /** * When the user logs off or the session expires, the session is deleted from redis * @param session */ @Override protected void doDelete(Session session) { //null validation if (session == null) { return; } //Delete the k-v of the specified SessionId from Redis redisTemplate.delete(PREFIX+session.getId()); } /** * shiro Obtain the Session object through sessionId and redis * @param sessionId * @return */ @Override protected Session doReadSession(Serializable sessionId) { if (sessionId == null) { return null; } //Read Session object from Redis Session session = redisTemplate.opsForValue().get(PREFIX+sessionId); return session; } }
4) Inject RedisSessionManager into SecurityManager
@Autowired private RedisTemplate redisTemplate; /** * Register RedisSessionDao in the container * @param redisTemplate * @return */ @Bean public SessionDAO redisSessionDAO(RedisTemplate redisTemplate) { return new RedisSessionDAO(redisTemplate); } /** * Inject custom CustomRealm into SecurityManager * Shiro Manage internal component instances through SecurityManager, and provide various security management services through it * @return */ @Bean public DefaultWebSecurityManager securityManager(CustomerRealm customRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // Custom Realm securityManager.setRealm(customRealm); // Rewrite the session manager and inject custom SessionDao DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager(); defaultWebSessionManager.setSessionDAO(redisSessionDAO(redisTemplate)); securityManager.setSessionManager(defaultWebSessionManager); return securityManager; }
So far, Shiro's cluster sharing Session has been completed