1. Introduction
In the last article “SpringBoot minimalist integration Shiro”, explained the process of SpringBoot minimalist integration Shiro, but because it is a minimalist integration, so some places are not suitable for the production environment, can be optimized, such as: distributed Session in the cluster environment; Each time a user is authorized, the user needs to go to the database for query.
Therefore, this article will be based on the previous article, through Redis to achieve the following functions:
- Session Implements the distributed Session function
- The user’s identity authentication information and authorization information is cached in Redis to avoid multiple queries to the database
2. Project structure
On the basis of the previous, the project structure is basically unchanged, except to add a ShirosessionManager.java, which is used to obtain the SessionId
3. Coding implementation
3.1 the pom import
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<! -- Add Redis dependencies -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
Copy the code
3.2 application. Yml
Added redis configuration
server:
port: 8903
spring:
application:
name: lab-user
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: JDBC: mysql: / / 127.0.0.1:3306 / laboratory? charset=utf8
username: root
password: root
redis:
host: 127.0. 01.
port: 6379
password: 123456
mybatis:
type-aliases-package: cn.ntshare.laboratory.entity
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
Copy the code
3.3 ShiroSessionManager. Java
/** * User-defined Session obtaining rules. The HTTP request header authToken carries the sessionId *. After a successful login, the Session sessionId */ is returned
public class ShiroSessionManager extends DefaultWebSessionManager {
public final static String HEADER_TOKEN_NAME = "token";
public ShiroSessionManager(a) {
super(a); }@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader(HEADER_TOKEN_NAME);
if (StringUtils.isEmpty(id)) {
// Get the SessionId from the cookie by default
return super.getSessionId(request, response);
} else {
// Get the sessionId from the Header
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
returnid; }}}Copy the code
3.4 ShiroConfig. Java
The document has been modified in the following aspects:
- The cache of identity authentication and authorization information is enabled
- RedisCacheManager and sessionManager are added
- Added redisSessionDAO
The code is as follows:
import cn.ntshare.laboratory.realm.UserRealm;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private Integer redisPort;
@Value("${spring.redis.password}")
private String redisPassword;
@Bean
public UserRealm userRealm(a) {
UserRealm userRealm = new UserRealm();
// Enable caching
userRealm.setCachingEnabled(true);
// Enable the authentication cache, that is, cache the AuthenticationInfo information
userRealm.setAuthenticationCachingEnabled(true);
// Set the identity cache name prefix
userRealm.setAuthenticationCacheName("authenticationCache");
// Enable authorization caching
userRealm.setAuthorizationCachingEnabled(true);
// This is the permission cache name prefix
userRealm.setAuthorizationCacheName("authorizationCache");
return userRealm;
}
@Bean
public DefaultWebSecurityManager securityManager(a) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
// Use Redis as the cache
securityManager.setCacheManager(redisCacheManager());
securityManager.setSessionManager(sessionManager());
return securityManager;
}
/** * Path filtering rule *@return* /
@Bean
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/login");
shiroFilterFactoryBean.setSuccessUrl("/");
Map<String, String> map = new LinkedHashMap<>();
// There is a sequence
map.put("/login"."anon");
map.put("/ * *"."authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/** * To enable Shiro annotation mode, you can add annotations to methods in Controller * such as @ *@param securityManager
* @return* /
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public SessionManager sessionManager(a) {
ShiroSessionManager sessionManager = new ShiroSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
@Bean
public RedisManager redisManager(a) {
RedisManager redisManager = new RedisManager();
redisManager.setHost(redisHost);
redisManager.setPort(redisPort);
if(redisPassword ! =null && !("").equals(redisPassword)) {
redisManager.setPassword(redisPassword);
}
return redisManager;
}
@Bean
public RedisSessionDAO redisSessionDAO(a) {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
// Set the cache name prefix
redisSessionDAO.setKeyPrefix("shiro:session:");
return redisSessionDAO;
}
@Bean
public RedisCacheManager redisCacheManager(a) {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
// Select the properties field as the cache identifier, in this case the Account field
redisCacheManager.setPrincipalIdFieldName("account");
// Set the information cache time
redisCacheManager.setExpire(86400);
returnredisCacheManager; }}Copy the code
3.5 UserRealm. Java
The authentication and authorization parts of this file are unchanged, only the method of clear caching has been added
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private PermissionService permissionService;
// User authorization
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("An authorization was executed.");
User user = (User) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
List<Role> roleList = roleService.findRoleByUserId(user.getId());
Set<String> roleSet = new HashSet<>();
List<Integer> roleIds = new ArrayList<>();
for (Role role : roleList) {
roleSet.add(role.getRole());
roleIds.add(role.getId());
}
// Add role information
authorizationInfo.setRoles(roleSet);
// Add permission information
List<String> permissionList = permissionService.findByRoleId(roleIds);
authorizationInfo.setStringPermissions(new HashSet<>(permissionList));
return authorizationInfo;
}
// User authentication
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken) throws AuthenticationException {
System.out.println("Authentication performed");
UsernamePasswordToken token = (UsernamePasswordToken) authToken;
User user = userService.findByAccount(token.getUsername());
if (user == null) {
return null;
}
return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
}
/** * Clears the current authorization cache *@param principalCollection
*/
@Override
public void clearCachedAuthorizationInfo(PrincipalCollection principalCollection) {
super.clearCachedAuthorizationInfo(principalCollection);
}
/** * Clears the current user authentication cache *@param principalCollection
*/
@Override
public void clearCachedAuthenticationInfo(PrincipalCollection principalCollection) {
super.clearCachedAuthenticationInfo(principalCollection);
}
@Override
public void clearCache(PrincipalCollection principalCollection) {
super.clearCache(principalCollection); }}Copy the code
3.6 LoginController. Java
@RestController
@RequestMapping("")
public class LoginController {
@PostMapping("/login")
public ServerResponseVO login(@RequestParam(value = "account") String account,
@RequestParam(value = "password") String password) {
Subject userSubject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(account, password);
try {
// Login authentication
userSubject.login(token);
// Encapsulate the return information
return ServerResponseVO.success(userSubject.getSession().getId());
} catch (UnknownAccountException e) {
return ServerResponseVO.error(ServerResponseEnum.ACCOUNT_NOT_EXIST);
} catch (DisabledAccountException e) {
return ServerResponseVO.error(ServerResponseEnum.ACCOUNT_IS_DISABLED);
} catch (IncorrectCredentialsException e) {
return ServerResponseVO.error(ServerResponseEnum.INCORRECT_CREDENTIALS);
} catch (Throwable e) {
e.printStackTrace();
returnServerResponseVO.error(ServerResponseEnum.ERROR); }}@GetMapping("/login")
public ServerResponseVO login(a) {
return ServerResponseVO.error(ServerResponseEnum.NOT_LOGIN_IN);
}
@GetMapping("/auth")
public String auth(a) {
return "Logged in successfully";
}
@GetMapping("/role")
@RequiresRoles("vip")
public String role(a) {
System.out.println("Test load balancing effect");
return "Test THE Vip role";
}
@GetMapping("/permission")
@RequiresPermissions(value = {"add"."update"}, logical = Logical.AND)
public String permission(a) {
return "Test Add and Update permissions"; }}Copy the code
After the above changes, we can already realize the functions of distributed session, cache identity information and cache authorization information. Let’s go to the test section.
4. Test the effect
4.1 Building a Cluster
Start two userApplications with port numbers 8903 and 8904
Nginx configuration is as follows:
server { server_name dev.ntshare.cn; location / { proxy_pass http://load.ntshare.cn; }} upstream load.ntshare. Cn {server 127.0.0.1:8903 weight=1; Server 127.0.0.1:8904 weight = 1; }Copy the code
4.2 Postman Access test
Log in as a VIP user
Check out Redis
Redis only has two caches at this time, one is the session cache, the other is the identity authentication information cache, and the key of the identity authentication cache uses the account information as the identifier
To access an interface that requires a VIP role, add a Header
Look at the number of caches in Redis:
Additional cache information for role authorization
After Redis is used as the data cache, the system only performs database queries during the first authentication and the first role authorization, and all subsequent operations are performed through the Redis cache.
4.3 Other User and interface Tests
slightly
5. To summarize
- rewrite
SessionManager
And the front end will add the sessionId to the request header to realize the distributed session of session. - By integrating Redis, Shiro framework puts authentication information and authorization information into Redis, avoiding the problem of repeatedly querying the database when the same user authenticates and authorizes multiple times.