1、依赖pom.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<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配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<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单点登录核心配置

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
<?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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class CasPac4jRealm extends Pac4jRealm {

@Autowired

private UserService userService;

@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 {

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());

}

}

@Override

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

String username = ((Pac4jPrincipal) principals.getPrimaryPrincipal()).getName();

return null;

}

5、JWT配置及实现

5.1 JWTFilter实现,所有请求都转由自定义的JWTFilter处理

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
public class JWTFilter extends BasicHttpAuthenticationFilter {

private Logger log = LoggerFactory.getLogger(this.getClass());

@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);

getSubject(request, response).login(token);

return true;

}

@Override

protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {

if (this.isLoginAttempt(request, response)) {

return false;

}

boolean allowed = false;

try {

allowed = executeLogin(request, response);

} catch (IllegalStateException e) {

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);

}

@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;

}

@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;

}

@Override

protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request,

ServletResponse response) {

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class JWTToken implements AuthenticationToken {

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的生成,解析

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
public class JwtUtils {

public static final String AUTH_HEADER = "Authorization";

public static final String SECRET_KEY = "sercret";

private static final long EXPIRE_TIME = 24 * 60 * 1000;

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);

return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);

} catch (JWTCreationException e) {

return null;

}catch (UnsupportedEncodingException ex) {

return null;

}

}

public static String getUsername(String token) {

try {

DecodedJWT jwt = JWT.decode(token);

return jwt.getClaim("username").asString();

} catch (JWTDecodeException e) {

return null;

}

}

public static Date getIssuedAt(String token) {

try {

DecodedJWT jwt = JWT.decode(token);

return jwt.getIssuedAt();

} catch (JWTDecodeException e) {

return null;

}

}

public static boolean isTokenExpired(String token) {

Date now = Calendar.getInstance().getTime();

DecodedJWT jwt = JWT.decode(token);

return jwt.getExpiresAt().before(now);

}

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地址,如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@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();

String token = JwtUtils.sign(username, JwtUtils.SECRET_KEY);

System.out.println("token=" + token);

JWTToken jwtToken = new JWTToken(token);

((HttpServletResponse) response).setHeader(JwtUtils.AUTH_HEADER, 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/