cas单点登录客户端集成整合(springmvc+pac4j+shiro+jwt)

2022-06-22 13:09:44

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/

  • 作者:poyi2008
  • 原文链接:https://blog.csdn.net/dengyl2008/article/details/102782337
    更新时间:2022-06-22 13:09:44