搭建:三个项目相互之间都是独立的,zuul-demo1、zuul-demo2作为两个服务,zuul-gateway根据路径转发请求给这两个服务,须携带请求头Authorization。
项目地址:https://gitee.com/zzhua195/zuul-shiro
一、zuul-demo1项目
1. 导入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zzhua</groupId>
<artifactId>zuul-demo1</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--shiro与spring整合 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--shiro与redis整合实现sessionDao -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
2. ShiroConfiguration
package com.zzhua.config;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.SessionListener;
import org.apache.shiro.session.mgt.SessionKey;
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.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.session.mgt.WebSessionKey;
import org.apache.shiro.web.util.WebUtils;
import org.crazycake.shiro.IRedisManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.util.StringUtils;
import javax.servlet.Filter;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
@Configuration
public class ShiroConfiguration {
@Value("${spring.redis.host:127.0.0.1}")
private String host;
@Value("${spring.redis.port:6379}")
private int port;
@Value("${spring.redis.timeout:6379}")
private int timeout;
@Bean
public Realm realm() {
return new MyRealm();
}
@Bean
public IRedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
redisManager.setTimeout(timeout);
return redisManager;
}
@Bean
@Primary
public RedisSessionDAO redisSessionDAO(IRedisManager redisManager) {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager);
// redisSessionDAO.setValueSerializer();
return redisSessionDAO;
}
// 自定义会话管理器
@Bean
public DefaultWebSessionManager sessionManager(RedisSessionDAO redisSessionDAO,
@Autowired(required = false) SessionListener sessionListener) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager() {
private static final String HEADER = "Authorization";
/**
* 此方法会被调用2次,
* 第一次是: 请求刚进来,正在resolveContextSession(subjectContext)
* 第二次是: 使用session存东西setAttribute(key,val),
* DelegatingSession会使用保存的sessionManager
* 并且会把key给传过来
*
* @param key
* @return
*/
@Override
public Serializable getSessionId(SessionKey key) {
if (WebUtils.isWeb(key)) {
WebSessionKey webSessionKey = (WebSessionKey) key;
HttpServletRequest req = (HttpServletRequest) webSessionKey.getServletRequest();
String header = req.getHeader(HEADER);
return StringUtils.isEmpty(header)?key.getSessionId():header;
}
// return super.getSessionId(key)
// 返回key的sessionId(不支持父类其它获取sessionId的方式,只允许请求头方式),
// 这里如果返回了不为null的结果,那么必须要保证根据这个结果能找到Session,否则Shiro默认会抛出异常,见(DefaultSessionManager#retrieveSession)
return key.getSessionId();
}
};
// 即不会给客户端写回sessionId
sessionManager.setSessionIdCookieEnabled(false);
// 即不会重写url路径(如果浏览器不支持cookie,JSESSIONID拼接在url上)
sessionManager.setSessionIdUrlRewritingEnabled(false);
// 设置sessionDao
sessionManager.setSessionDAO(redisSessionDAO);
// 每隔半分钟检测一次session
sessionManager.setSessionValidationInterval(30000);
// 设置自定义的对应的会话监听器
sessionManager.setSessionListeners(Arrays.asList(sessionListener));
return sessionManager;
}
@Bean
public SecurityManager securityManager(Realm realm, SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setSessionManager(sessionManager);
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
// 注册自定义filter
HashMap<String, Filter> filters = new HashMap<>();
// 在springboot中,不能把这个filter交给spring管理,
// 否则它会直接被嵌入到web,有可能先于shiroFilter执行,而导致不能拿到Subject
MyFilter myFilter = new MyFilter();
filters.put("myFilter", myFilter);
shiroFilter.setFilters(filters);
LinkedHashMap<String, String> chainDefMap = new LinkedHashMap<>();
chainDefMap.put("/free", "anon"); // 匿名filter
chainDefMap.put("/login", "anon"); // 匿名filter
chainDefMap.put("/**", "myFilter"); // 自定义filter
// 设置拦截器链
shiroFilter.setFilterChainDefinitionMap(chainDefMap);
// 设置安全管理器
shiroFilter.setSecurityManager(securityManager);
return shiroFilter;
}
@Bean
public AuthorizationAttributeSourceAdvisor advisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
return advisor;
}
}
3. MyFilter
public class MyFilter extends AccessControlFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
Subject subject = getSubject(request, response);
return subject.isAuthenticated();
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
ObjectMapper mapper = new ObjectMapper();
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write(mapper.writeValueAsString("未登录,不能访问"));
return false;
}
}
4. MyRealm
public class MyRealm extends AuthorizingRealm {
/**
* 获取授权信息
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authInfo = new SimpleAuthorizationInfo();
User user = (User) principals.getPrimaryPrincipal();
if ("zhangsan".equals(user.getName())) {
authInfo.addStringPermission("get:a");
}
if ("lisi".equals(user.getName())) {
authInfo.addStringPermission("get:b");
}
if ("wangwu".equals(user.getName())) {
authInfo.addStringPermission("get:a");
authInfo.addStringPermission("get:b");
authInfo.addRoles(Collections.singletonList("role:admin"));
}
return authInfo;
}
/**
* 获取认证信息
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
System.out.println("登录名:" + upToken.getUsername());
System.out.println("密码:" + Arrays.toString(upToken.getPassword()));
if ("zhangsan".equals(upToken.getUsername())) {
return new SimpleAuthenticationInfo(new User("zhangsan", 23), "123456", this.getName());
} else if ("lisi".equals(upToken.getUsername())) {
return new SimpleAuthenticationInfo(new User("lisi", 23), "123456", this.getName());
} else if ("wangwu".equals(upToken.getUsername())) {
return new SimpleAuthenticationInfo(new User("wangwu", 23), "123456", this.getName());
}
return null;
}
}
@Primary
@Component
public class MySessionListener implements SessionListener {
private static CopyOnWriteArrayList<Session> sessions = new CopyOnWriteArrayList<>();
public static CopyOnWriteArrayList<Session> getSessions() {
return sessions;
}
// 拥有身份,创建session
@Override
public void onStart(Session session) {
sessions.add(session);
}
// ================检测session周期:默认一个小时=====================
// 检测session valid
@Override
public void onStop(Session session) {
sessions.removeIf(s -> {
boolean flag = Objects.equals(session.getId(), s.getId());
Object principals = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (principals != null) {
Object primaryPrincipal = ((PrincipalCollection) principals).getPrimaryPrincipal();
User user = (User) primaryPrincipal;
System.out.println(user.getName() + " 账号已被停用");
}
return flag;
});
}
// 检测session过期
@Override
public void onExpiration(Session session) {
sessions.removeIf(s -> {
boolean flag = Objects.equals(session.getId(), s.getId());
Object principals = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (principals != null) {
Object primaryPrincipal = ((PrincipalCollection) principals).getPrimaryPrincipal();
User user = (User) primaryPrincipal;
System.out.println(user.getName() + " 账号已过期");
}
return flag;
});
}
}
5. MySessionListener
@Primary
@Component
public class MySessionListener implements SessionListener {
private static CopyOnWriteArrayList<Session> sessions = new CopyOnWriteArrayList<>();
public static CopyOnWriteArrayList<Session> getSessions() {
return sessions;
}
// 拥有身份,创建session
@Override
public void onStart(Session session) {
sessions.add(session);
}
// ================检测session周期:默认一个小时=====================
// 检测session valid
@Override
public void onStop(Session session) {
sessions.removeIf(s -> {
boolean flag = Objects.equals(session.getId(), s.getId());
Object principals = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (principals != null) {
Object primaryPrincipal = ((PrincipalCollection) principals).getPrimaryPrincipal();
User user = (User) primaryPrincipal;
System.out.println(user.getName() + " 账号已被停用");
}
return flag;
});
}
// 检测session过期
@Override
public void onExpiration(Session session) {
sessions.removeIf(s -> {
boolean flag = Objects.equals(session.getId(), s.getId());
Object principals = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (principals != null) {
Object primaryPrincipal = ((PrincipalCollection) principals).getPrimaryPrincipal();
User user = (User) primaryPrincipal;
System.out.println(user.getName() + " 账号已过期");
}
return flag;
});
}
}
6. SessionController
package com.zzhua.config;
import com.zzhua.pojo.User;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SimpleSession;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.support.DefaultSubjectContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@RestController
public class SessionController {
@Autowired
SessionDAO sessionDAO;
@RequiresRoles("role:admin")
@RequestMapping("stopSession/{sessionId}")
public String stopSession(@PathVariable("sessionId") String sessionId) {
Session session = sessionDAO.readSession(sessionId);
if (session == null) {
return "未找到该会话";
}
Object principals = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
String uname = "";
if (principals != null) {
Object user = ((PrincipalCollection) principals).getPrimaryPrincipal();
uname = ((User) user).getName();
}
sessionDAO.delete(session);
return "停用会话id:"+session.getId()+",用户为: " + uname;
}
@RequiresRoles("role:admin")
@RequestMapping("getAllSessions")
public List<String> getAllSessions() {
Collection<Session> sessions = sessionDAO.getActiveSessions();
List<String> nameList = new ArrayList<>();
sessions.stream().forEach(session -> {
Object principals = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (principals != null) {
Object user = ((PrincipalCollection) principals).getPrimaryPrincipal();
String uname = ((User) user).getName();
nameList.add(uname);
}
});
return nameList;
}
}
7. TestController
@Validated
@RestController
public class Test1Controller {
@RequestMapping("test")
public Object test1(@RequestParam(value = "name",required = false) String name,
@RequestHeader(value = "token1",required = false) String token1,
@CookieValue(name = "ck",required = false) String ck,
@RequestBody(required = false) Map<String, Object> map) {
System.out.println("请求参数: " + name);
System.out.println("请求头: " + token1);
System.out.println("请求体: " + map);
System.out.println("cookie: " + ck);
return "test1";
}
@RequestMapping("free")
public Object free() {
return "free";
}
@RequiresPermissions("get:a")
@RequestMapping("a")
public Object a() {
return "a";
}
@RequiresPermissions("get:b")
@RequestMapping("b")
public Object b() {
return "b";
}
}
8.User
import java.io.Serializable;
public class User implements Serializable {
private String name;
private Integer age;
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
public User() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
9. MyControllerAdvice
@RestControllerAdvice
public class MyControllerAdvice {
@ExceptionHandler(Exception.class)
public Object object(Exception ex) {
return ex.getMessage();
}
}
二、zuul-demo2
将zuul-demo1复制一遍改个端口号,即可(后面抽取下shiro到公共模块)
三、zuul-gateway
将zuul-demo1的shiro配置拷一份过来
1.pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zzhua</groupId>
<artifactId>zuul-gateway</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.7.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!--shiro与spring整合 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--shiro与redis整合实现sessionDao -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
2. application.yml
server:
port: 8080
zuul:
routes:
zuul-demo1:
path: /zuul-demo1/**
url: http://127.0.0.1:9091
zuul-demo2:
path: /zuul-demo2/**
url: http://127.0.0.1:9092
sensitive-headers: #这里须为空,会将请求头都携带过去
3.启动类
@EnableZuulProxy
@SpringBootApplication
public class ZuulGatewayApp {
public static void main(String[] args) {
SpringApplication.run(ZuulGatewayApp.class, args);
}
}
4.LoginController
package com.zzhua.controller;
import com.zzhua.pojo.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.subject.Subject;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
import java.util.Map;
@Validated
@RestController
public class LoginController {
@RequestMapping("login")
public Object login(@NotBlank(message = "uname can't be blank") String uname,
@NotBlank(message = "pword can't be blank")String pword) {
Subject subject = SecurityUtils.getSubject();
subject.login(new UsernamePasswordToken(uname, pword));
// 登录成功之后,返回sessionId给前端
Serializable sessionId = subject.getSession().getId();
return sessionId;
}
@RequestMapping("getCurrUser")
public User getCurrUser() {
Object principal = SecurityUtils.getSubject().getPrincipal();
System.out.println(principal);
return ((User) principal);
}
}