1、依赖pom.xml
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-cas</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>io.buji</groupId>
<artifactId>buji-pac4j</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-jwt</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
2、web.xml配置
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetBeanName</param-name>
<param-value>shiroFilter</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
3、spring-shiro.xml,pac4j整合shiro单点登录核心配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
default-lazy-init="true">
<description>Shiro pac4j Configuration</description>
<!-- 配置shiro过滤器工厂 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- 配置注入安全管理对象 -->
<property name="securityManager" ref="securityManager"/>
<!-- 配置过滤器 -->
<property name="filters">
<map>
<!-- 1. 安全过滤器,拦截需要登录的URL -->
<entry key="security">
<bean class="io.buji.pac4j.filter.SecurityFilter">
<property name="config" ref="config"/>
</bean>
</entry>
<!-- 2. 回调过滤器,完成ticket验证 -->
<entry key="callback">
<bean class="io.buji.pac4j.filter.CallbackFilter">
<property name="config" ref="config"/>
<!-- 验证通过后默认重定向URL -->
<property name="defaultUrl" value="http://192.168.0.41:8080/wcm/index"/>
</bean>
</entry>
<!-- 3. 退出过滤器,拦截需要退出的URL -->
<entry key="logout">
<bean class="io.buji.pac4j.filter.LogoutFilter">
<property name="config" ref="config"/>
<!-- 中央退出 -->
<property name="centralLogout" value="true"/>
<!-- 本地退出 -->
<property name="localLogout" value="true"/>
<!-- 退出成功后默认重定向URL -->
<property name="defaultUrl" value="http://192.168.1.50:85/cas/login?service=http://192.168.0.41:8080/wcm/index"/>
</bean>
</entry>
<-- JWTFilter配置 -->
<entry key="jwt">
<bean class="com.tfrd.shiro.JWTFilter"></bean>
</entry>
</map>
</property>
<!-- 配置URL过滤器链(配置顺序为自上而下) -->
<property name="filterChainDefinitions">
<value>
/ = security
/index = security
/logout = logout
/callback = callback
/** = jwt
</value>
</property>
</bean>
<!-- pac4j配置 -->
<bean id="config" class="org.pac4j.core.config.Config">
<constructor-arg name="client" ref="casClient"/>
</bean>
<!-- 配置CAS客户端 -->
<bean id="casClient" class="org.pac4j.cas.client.CasClient">
<!-- 设置cas服务端信息 -->
<property name="configuration" ref="casConfiguration"/>
<!-- 登录成功后重定向回来的请求URL
<property name="callbackUrl" value="http://192.168.0.41:8080/wcm/callback?client_name=CasClient"/>
<!-- 设置客户端名称(client_name=CasClient) -->
<property name="name" value="CasClient"/>
</bean>
<!-- 配置cas服务端信息 -->
<bean id="casConfiguration" class="org.pac4j.cas.config.CasConfiguration">
<!-- CAS服务端登录请求URL -->
<property name="loginUrl" value="http://192.168.1.50:85/cas/login"/>
<!-- CAS服务端请求URL前缀-->
<property name="prefixUrl" value="http://192.168.1.50:85/cas/"/>
</bean>
<!-- 自定义身份认证域 -->
<bean id="pac4jRealm" class="com.tfrd.shiro.CasPac4jRealm"/>
<!-- 基于pac4j的Subject工厂 -->
<bean id="pac4jSubjectFactory" class="io.buji.pac4j.subject.Pac4jSubjectFactory"></bean>
<!-- 配置安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!--单个realm使用realm,如果有多个realm,使用realms属性代替-->
<property name="realm" ref="pac4jRealm" />
<!-- 缓存管理 -->
<property name="cacheManager" ref="cacheManager" />
<!-- session 管理器 -->
<property name="sessionManager" ref="sessionManager" />
<property name="subjectFactory" ref="pac4jSubjectFactory" />
</bean>
<!-- 以下是其它配置 -->
<!-- session管理器 -->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!-- 超时时间 -->
<property name="globalSessionTimeout" value="1800000"/>
<!-- session存储的实现 -->
<property name="sessionDAO" ref="shiroSessionDao"/>
<!-- sessionIdCookie的实现,用于重写覆盖容器默认的JSESSIONID -->
<property name="sessionIdCookie" ref="sharesession"/>
<!-- 定时检查失效的session -->
<property name="sessionValidationSchedulerEnabled" value="true" />
</bean>
<!-- sessionIdCookie的实现,用于重写覆盖容器默认的JSESSIONID -->
<bean id="sharesession" class="org.apache.shiro.web.servlet.SimpleCookie">
<!-- cookie的name,对应的默认是 JSESSIONID -->
<constructor-arg name="name" value="SHAREJSESSIONID"/>
<!-- 记住我cookie生效时间30天 -->
<property name="maxAge" value="2592000" />
</bean>
<!-- session存储的实现 -->
<bean id="shiroSessionDao" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO" />
<!-- 用户授权信息Cache -->
<bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager" />
<!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
<!-- AOP式方法级权限检查(配置DefaultAdvisorAutoProxyCreator,必须配置了lifecycleBeanPostProcessor才能使用) -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor">
<property name="proxyTargetClass" value="true" />
</bean>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager" />
</bean>
<bean id="formAuthenticationFilter" class="com.tfrd.shiro.CustomFormAuthenticationFilter">
<property name="usernameParam" value="username" />
<property name="passwordParam" value="password" />
</bean>
</beans>
4、重写Pac4jRealm,自定义CasPac4jRealm继承Pac4jRealm
/**
* 自定义身份认证域
*/
public class CasPac4jRealm extends Pac4jRealm {
@Autowired
private UserService userService;
/**
* 重点:注意,这里必须判断,cas登录时,token类型为AuthenticationToken
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
if (token instanceof JWTToken) {
return token instanceof JWTToken;
}
return token instanceof AuthenticationToken;
}
/**
* 验证用户身份
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
if (!(authenticationToken instanceof JWTToken)) {
final Pac4jToken pac4jToken = (Pac4jToken) authenticationToken;
final List<CommonProfile> commonProfileList = pac4jToken.getProfiles();
final CommonProfile commonProfile = commonProfileList.get(0);
final Pac4jPrincipal principal = new Pac4jPrincipal(commonProfileList, getPrincipalNameAttribute());
final PrincipalCollection principalCollection = new SimplePrincipalCollection(principal, getName());
return new SimpleAuthenticationInfo(principalCollection, commonProfileList.hashCode());
} else {
// 这里的 token是从 JWTFilter 的 executeLogin 方法传递过来的
System.out.println(authenticationToken.getCredentials());
String token = (String) authenticationToken.getCredentials();
String username = JwtUtils.getUsername(token);
UserModel user = userService.getBeanByAccount(username);
if (user == null) {
throw new AuthenticationException("用户名或密码错误");
}
if (!JwtUtils.verify(token, username, JwtUtils.SECRET_KEY)) {
throw new AuthenticationException("token校验不通过");
}
return new SimpleAuthenticationInfo(token, token, getName());
}
}
/**
* 设置角色和权限
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取登录用户名
String username = ((Pac4jPrincipal) principals.getPrimaryPrincipal()).getName();
// DOTO
return null;
}
5、JWT配置及实现
5.1 JWTFilter实现,所有请求都转由自定义的JWTFilter处理
public class JWTFilter extends BasicHttpAuthenticationFilter {
private Logger log = LoggerFactory.getLogger(this.getClass());
/**
* 判断用户是否想要登入。
* 检测header里面是否包含Authorization字段即可
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader("Authorization");
return authorization == null;
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String authorization = httpServletRequest.getHeader("Authorization");
JWTToken token = new JWTToken(authorization);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(token);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
// 原用来判断是否是登录请求,用来检测Header中是否包含 JWT token 字段
if (this.isLoginAttempt(request, response)) {
return false;
}
boolean allowed = false;
try {
// 检测Header里的 JWT token内容是否正确,尝试使用 token进行登录
allowed = executeLogin(request, response);
} catch (IllegalStateException e) { // not found any token
log.error("Not found any token");
} catch (Exception e) {
log.error("Error occurs when login", e);
}
return allowed || super.isPermissive(mappedValue);
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* isAccessAllowed()方法返回false,会进入该方法,表示拒绝访问
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletResponse httpResponse = WebUtils.toHttp(servletResponse);
httpResponse.setCharacterEncoding("UTF-8");
httpResponse.setContentType("application/json;charset=UTF-8");
httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
PrintWriter writer = httpResponse.getWriter();
writer.write("{\"state\": 401, \"message\": \"UNAUTHORIZED\"}");
fillCorsHeader(WebUtils.toHttp(servletRequest), httpResponse);
return false;
}
/**
* Shiro 利用 JWT token 登录成功,会进入该方法
*/
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,
ServletResponse response) throws Exception {
HttpServletResponse httpResponse = WebUtils.toHttp(response);
String newToken = null;
if (token instanceof JWTToken) {
newToken = JwtUtils.refreshTokenExpired(token.getCredentials().toString(), JwtUtils.SECRET_KEY);
}
if (newToken != null) {
httpResponse.setHeader(JwtUtils.AUTH_HEADER, newToken);
}
return true;
}
/**
* Shiro 利用 JWT token 登录失败,会进入该方法
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request,
ServletResponse response) {
// 此处直接返回 false ,交给后面的 onAccessDenied()方法进行处理
return false;
}
/**
* 添加跨域支持
*/
protected void fillCorsHeader(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,HEAD");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
}
}
5.2 JWTToken 实现 AuthenticationToken
public class JWTToken implements AuthenticationToken {
// Token
private String token;
public JWTToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
5.3 JwtUtils 工具类,token的生成,解析
public class JwtUtils {
// 请求头
public static final String AUTH_HEADER = "Authorization";
// 私密
public static final String SECRET_KEY = "sercret";
// 过期时间1天
private static final long EXPIRE_TIME = 24 * 60 * 1000;
/**
* 验证token是否正确
*/
public static boolean verify(String token, String username, String secret) {
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
verifier.verify(token);
return true;
} catch (JWTVerificationException exception) {
return false;
}catch (UnsupportedEncodingException ex) {
return false;
}
}
/**
* 生成签名
*/
public static String sign(String username, String secret) {
try {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username信息
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
} catch (JWTCreationException e) {
return null;
}catch (UnsupportedEncodingException ex) {
return null;
}
}
/**
* Get username from TOKEN
* @return token contains username information
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 获取 token的签发时间
*/
public static Date getIssuedAt(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getIssuedAt();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 验证 token是否过期
*/
public static boolean isTokenExpired(String token) {
Date now = Calendar.getInstance().getTime();
DecodedJWT jwt = JWT.decode(token);
return jwt.getExpiresAt().before(now);
}
/**
* 刷新 token的过期时间
*/
public static String refreshTokenExpired(String token, String secret) {
DecodedJWT jwt = JWT.decode(token);
Map<String, Claim> claims = jwt.getClaims();
try {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTCreator.Builder builer = JWT.create().withExpiresAt(date);
for (Map.Entry<String, Claim> entry : claims.entrySet()) {
builer.withClaim(entry.getKey(), entry.getValue().asString());
}
return builer.sign(algorithm);
} catch (JWTCreationException e) {
return null;
}catch (UnsupportedEncodingException ex) {
return null;
}
}
}
6,LoginController,当未登录时,会跳转到cas服务器的登录页面进行登录,登录成功指向url上配置的service地址,如下
/**
* 登录成功跳转前端页面
*
* @param attr
* @return
*/
@RequestMapping(value = "/index", method = RequestMethod.GET)
public void index(HttpServletResponse response) {
try {
// 获取用户身份
Pac4jPrincipal p = SecurityUtils.getSubject().getPrincipals().oneByType(Pac4jPrincipal.class);
CommonProfile profile = p.getProfile();
String username = profile.getUsername();
// 生成token
String token = JwtUtils.sign(username, JwtUtils.SECRET_KEY);
System.out.println("token=" + token);
JWTToken jwtToken = new JWTToken(token);
// 将签发的 JWT token 设置到 HttpServletResponse 的 Header中,并重写向vue前端页面
((HttpServletResponse) response).setHeader(JwtUtils.AUTH_HEADER, token);
// response.sendRedirect("http://192.168.0.41:8088/#/stats/casindex?token=" + token);
response.sendRedirect("http://192.168.0.41:8088/#/stats/casindex");
} catch (Exception e) {
e.getStackTrace();
}
}
至此pac4j整合shiro的单点登录已完成。
参考连接:http://www.andrew-programming.com/2019/01/23/springboot-integrate-with-jwt-and-apache-shiro/