1. Введение
В предыдущей статье "Минималистская интеграция SpringBoot с Широ", объясняет процесс минималистской интеграции SpringBoot с Широ, но поскольку это минималистская интеграция, некоторые места не подходят для производственных сред и могут быть оптимизированы, например: распределенные сеансы сеанса в кластерной среде; каждый раз, когда пользователь авторизуется, Все, что нужно, чтобы перейти к запросу базы данных и другие вопросы.
Поэтому в этой статье будут реализованы следующие функции через Redis на основе предыдущей статьи:
- Реализовать функцию распределенного сеанса сеанса
- Кэшируйте информацию об аутентификации пользователя и информацию об авторизации в Redis, чтобы избежать многократных запросов к базе данных.
2. Структура проекта
На прежней основе структура проекта принципиально не менялась, просто добавлялсяShiroSessionManager.java
, используется для получения SessionId
3. Реализация кода
3.1 импорт пом
<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>
<!--增加Redis相关依赖-->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
3.2 application.yml
Добавлена конфигурация Redis
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.0.1
port: 6379
password: 123456
mybatis:
type-aliases-package: cn.ntshare.laboratory.entity
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
3.3 ShiroSessionManager.java
/**
* 自定义Session获取规则,采用http请求头authToken携带sessionId的方式
* 登录成功后,会返回会话的sessionId,前端需要在请求头中加入该sessionId
*/
public class ShiroSessionManager extends DefaultWebSessionManager {
public final static String HEADER_TOKEN_NAME = "token";
public ShiroSessionManager() {
super();
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader(HEADER_TOKEN_NAME);
if (StringUtils.isEmpty(id)) {
// 按照默认规则从cookie中获取SessionId
return super.getSessionId(request, response);
} else {
// 从Header头中获取sessionId
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
}
}
}
3.4 ShiroConfig.java
Документ был изменен в следующих аспектах на основе предыдущего:
- Включить кэширование информации об аутентификации и авторизации
- Добавлен redisCacheManager, sessionManager
- Добавлен redisSessionDAO
код показывает, как показано ниже:
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() {
UserRealm userRealm = new UserRealm();
// 开启缓存
userRealm.setCachingEnabled(true);
// 开启身份验证缓存,即缓存AuthenticationInfo信息
userRealm.setAuthenticationCachingEnabled(true);
// 设置身份缓存名称前缀
userRealm.setAuthenticationCacheName("authenticationCache");
// 开启授权缓存
userRealm.setAuthorizationCachingEnabled(true);
// 这是权限缓存名称前缀
userRealm.setAuthorizationCacheName("authorizationCache");
return userRealm;
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
// 使用Redis作为缓存
securityManager.setCacheManager(redisCacheManager());
securityManager.setSessionManager(sessionManager());
return securityManager;
}
/**
* 路径过滤规则
* @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<>();
// 有先后顺序
map.put("/login", "anon");
map.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/**
* 开启Shiro注解模式,可以在Controller中的方法上添加注解
* 如:@
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public SessionManager sessionManager() {
ShiroSessionManager sessionManager = new ShiroSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(redisHost);
redisManager.setPort(redisPort);
if (redisPassword != null && !("").equals(redisPassword)) {
redisManager.setPassword(redisPassword);
}
return redisManager;
}
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
// 设置缓存名前缀
redisSessionDAO.setKeyPrefix("shiro:session:");
return redisSessionDAO;
}
@Bean
public RedisCacheManager redisCacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
// 选择属性字段作为缓存标识,这里选择account字段
redisCacheManager.setPrincipalIdFieldName("account");
// 设置信息缓存时间
redisCacheManager.setExpire(86400);
return redisCacheManager;
}
}
3.5 UserRealm.java
Часть аутентификации и авторизации этого файла не изменилась, был добавлен только метод очистки кеша.
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private PermissionService permissionService;
// 用户授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了一次授权");
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());
}
// 放入角色信息
authorizationInfo.setRoles(roleSet);
// 放入权限信息
List<String> permissionList = permissionService.findByRoleId(roleIds);
authorizationInfo.setStringPermissions(new HashSet<>(permissionList));
return authorizationInfo;
}
// 用户认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken) throws AuthenticationException {
System.out.println("执行了身份认证");
UsernamePasswordToken token = (UsernamePasswordToken) authToken;
User user = userService.findByAccount(token.getUsername());
if (user == null) {
return null;
}
return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
}
/**
* 清除当前授权缓存
* @param principalCollection
*/
@Override
public void clearCachedAuthorizationInfo(PrincipalCollection principalCollection) {
super.clearCachedAuthorizationInfo(principalCollection);
}
/**
* 清除当前用户身份认证缓存
* @param principalCollection
*/
@Override
public void clearCachedAuthenticationInfo(PrincipalCollection principalCollection) {
super.clearCachedAuthenticationInfo(principalCollection);
}
@Override
public void clearCache(PrincipalCollection principalCollection) {
super.clearCache(principalCollection);
}
}
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 {
// 登录验证
userSubject.login(token);
// 封装返回信息
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();
return ServerResponseVO.error(ServerResponseEnum.ERROR);
}
}
@GetMapping("/login")
public ServerResponseVO login() {
return ServerResponseVO.error(ServerResponseEnum.NOT_LOGIN_IN);
}
@GetMapping("/auth")
public String auth() {
return "已成功登录";
}
@GetMapping("/role")
@RequiresRoles("vip")
public String role() {
System.out.println("测试负载均衡效果");
return "测试Vip角色";
}
@GetMapping("/permission")
@RequiresPermissions(value = {"add", "update"}, logical = Logical.AND)
public String permission() {
return "测试Add和Update权限";
}
}
После вышеуказанных изменений функции распределенного сеанса, кешированной информации об идентичности и кешированной информации об авторизации уже могут быть реализованы.Теперь давайте введем тестовую ссылку.
4. Проверьте эффект
4.1 Создание кластера
Запустите два UserApplications с номерами портов 8903 и 8904.
Nginx настроен следующим образом:
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;
}
4.2 Тест доступа почтальона
Войти под VIP-пользователем
Посмотреть ситуацию с Redis
В настоящее время Redis имеет только два кеша: один — кеш сеанса, другой — кеш информации аутентификации, а информация об учетной записи используется в качестве идентификатора в ключе кеша аутентификации.
Получите доступ к интерфейсу, для которого требуется роль VIP, обратите внимание на добавление заголовка
Посмотрите на количество кешей в Redis:
Есть еще одна кешированная информация для авторизации роли
После использования Redis в качестве кеша данных система будет запрашивать базу данных только при первой аутентификации и авторизации первой роли, а все последующие операции будут проходить через кеш Redis.
4.3 Другие тесты пользователя и интерфейса
немного
5. Резюме
- переписать
SessionManager
, настройте правила получения sessionId, а внешний интерфейс добавляет sessionId в заголовок запроса для реализации распределенного сеанса сеанса. - Интегрируя Redis, платформа Shiro помещает информацию об аутентификации и авторизации в Redis, избегая проблемы повторных запросов к базе данных для многократной аутентификации и авторизации одного и того же пользователя.