【SpringSecurity OAuth2 JWT】實現SSO單點登入 第一篇
一、 概述
本文使用Springsecurity、Oauth2 + JWT實現單點登入功能。
繼作者上一篇文章:Oauth2 + JWT 實現 SSO 單點登入
本文為進階篇,更細緻的實現了 Springsecurity安全框架的 各部分handler處理,讓系統執行起來更加細緻,靈活。
二、架構參考
1. 使用架構
- springboot 2.3.1
- springSecurity
- oauth2 jwt
- mybatis plus
- ehcache
- swagger
- druid
- thymelef + layui
2. SSO 時序圖
三、程式碼參考
1. Server端:
主要實現 “授權伺服器、資源伺服器、自定義登入校驗、生成token” 等,後續整合RBAC許可權管理,
閒話不多說,上程式碼:
- pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
- WebSecurityConfig.java
- 實現訪問攔截、security hanler定義、記住密碼等功能:
@Autowired
@Qualifier("resourceServerRequestMatcher")
private RequestMatcher resources;
@Override
protected void configure(HttpSecurity http) throws Exception {
RequestMatcher nonResoures = new NegatedRequestMatcher(resources);
http.requestMatcher(nonResoures).authorizeRequests()
// http.authorizeRequests()
.antMatchers("/swagger-resources/**", "/PearAdmin/**", "/component/**",
"/admin/**", "/**/*.html", "/**/*.css", "/**/*.js", "/swagger-ui.html",
"/webjars/**", "/v2/**", "/druid/**").permitAll()
.anyRequest().authenticated() // 其他地址的訪問均需驗證許可權
.and()
.formLogin()
.loginPage("/login")
//攔截的請求
// .loginProcessingUrl("/login")
// 登入成功處理
.successHandler(mySuccessHandler)
// 登入失敗處理
.failureHandler(myFailureHandler)
.permitAll()
.and()
// 記住密碼
.rememberMe()
.rememberMeParameter("rememberme")
.tokenValiditySeconds(2 * 24 * 60 * 60)
.and()
.logout()
.logoutSuccessHandler(myLogoutHandler)
.and().cors()
.and()
.csrf().disable() // 防止iframe 造成跨域
.headers()
.frameOptions()
.disable();
// 禁用快取
http.headers().cacheControl();
// 無權訪問 JSON 格式的資料
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 身份認證介面
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
- MyAuthenticationSuccessHandler
- 登入成功應答資訊:
@Component("MyAuthenticationSuccessHandler")
@Slf4j
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
//輸出結果
Result result = Result.ok().message("登入成功");
response.getWriter().write(JSON.toJSONString(result));
}
}
- MyAuthenticationFailureHandler
- 登入失敗應答資訊:
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
//修改編碼格式
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setContentType("application/json");
if (e instanceof BadCredentialsException){
httpServletResponse.getWriter().write(JSON.toJSONString(Result.error().message("使用者名稱或密碼錯誤")));
}else {
httpServletResponse.getWriter().write(JSON.toJSONString(Result.error().message(e.getMessage())));
}
}
}
- MyLogoutSuccessHandler
- 登出應答資訊:
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.sendRedirect(request.getHeader("referer"));
}
}
- RestfulAccessDeniedHandler
- 無許可權時、應答資訊:
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException e) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtils.toJSONString(Result.error().message(e.getMessage())));
response.getWriter().flush();
}
}
- 自定義登入校驗
- 賬號、密碼校驗、使用者許可權獲取
@Service @Slf4j public class UserDetailsServiceImpl implements UserDetailsService { @Resource private UserService userService; @Resource private RoleUserService roleUserService; @Resource private RoleService roleService; @Resource private MenuDao menuDao; @Override public JwtUserDto loadUserByUsername(String userName) { //根據使用者名稱獲取使用者 MyUser user = userService.getUserByName(userName); if (user == null) { throw new BadCredentialsException("使用者名稱或密碼錯誤"); } else if (user.getStatus().equals(MyUser.Status.LOCKED)) { throw new LockedException("使用者被鎖定,請聯絡管理員解鎖"); } List<GrantedAuthority> grantedAuthorities = new ArrayList<>(); List<MenuIndexDto> list = menuDao.listByUserId(user.getUserId()); List<String> collect = list.stream().map(MenuIndexDto::getPermission).collect(Collectors.toList()); for (String authority : collect) { if (!("").equals(authority) & authority != null) { GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(authority); grantedAuthorities.add(grantedAuthority); } } //將使用者所擁有的許可權加入GrantedAuthority集合中 JwtUserDto loginUser = new JwtUserDto(user, grantedAuthorities); return loginUser; } }
- 認證伺服器
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
security.tokenKeyAccess("isAuthenticated()");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.accessTokenConverter(jwtAccessTokenConverter());
endpoints.tokenStore(jwtTokenStore());
}
@Bean
public JwtTokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("zetor"); // Sets the JWT signing key
return jwtAccessTokenConverter;
}
}
- 登入頁面
<form th:action="@{/login}" method="post">
<p style="color: red" id="security-message" th:if="${param.error}"
th:text="${session['SPRING_SECURITY_LAST_EXCEPTION'].message}">
<div class="form-group">
<label>使用者名稱</label>
<input type="text" class="form-control" id="uname" placeholder="Username">
<input type="text" class="form-control" value="admin" style="display:none" id="username" name="username">
</div>
<div class="form-group">
<label>密碼</label>
<input type="password" class="form-control" name="password" placeholder="Password">
</div>
<div class="form-group">
<label>賬套</label>
<select class="form-control input-s-sm inline" name="sobSel" id="sobSel">
<option value="" selected>請選擇</option>
<option th:each="ss:${sobs}" th:value="${ss.sobCode}" th:text="${ss.sobName}"></option>
</select>
</div>
<div class="checkbox">
<label>
<input type="checkbox"> 記住密碼
</label>
<label class="pull-right">
<a href="#">忘記密碼?</a>
</label>
</div>
<button type="submit" class="btn btn-success btn-flat m-b-30 m-t-30" style="font-size: 18px;">登入
</button>
</form>
2. Client端 :
- pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
- 攔截器
@EnableOAuth2Sso
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${exit_url}")
private String exit_url;
@Autowired
@Qualifier("resourceServerRequestMatcher")
private RequestMatcher resources;
@Override
protected void configure(HttpSecurity http) throws Exception {
RequestMatcher nonResoures = new NegatedRequestMatcher(resources);
http.requestMatcher(nonResoures).authorizeRequests()
.anyRequest().authenticated()
.and()
.logout().logoutUrl("/logout").logoutSuccessUrl(exit_url)
.and()
.cors()
.and()
.csrf().disable();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
//允許帶憑證
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
//對所有URL生效
source.registerCorsConfiguration("/**", config);
return source;
}
}
-
資源伺服器(校驗token)
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Bean("resourceServerRequestMatcher")
public RequestMatcher resources() {
return new AntPathRequestMatcher("/api/**");
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatcher(resources()).authorizeRequests()
.anyRequest().authenticated()
.and()
.cors()
.and()
.csrf().disable();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
resources.tokenServices(tokenServices());
}
@Bean
public JwtTokenStore jwtTokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("zetor");
return converter;
}
/**
* resourceServerTokenServices 類的例項,用來實現令牌服務。
*
* @return
*/
@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(jwtTokenStore());
return defaultTokenServices;
}
}
-
Client Main 啟動呼叫
@Bean public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oauth2ClientContext, OAuth2ProtectedResourceDetails details) { return new OAuth2RestTemplate(details, oauth2ClientContext); }
-
首頁controller(此處支援前後端分離)
@RequestMapping("/")
public String index(Authentication authentication, Model model) {
OAuth2AuthenticationDetails detail = (OAuth2AuthenticationDetails) authentication.getDetails();
log.info("【登入成功】username:{}, sessonId:{}, {}", authentication.getPrincipal(), detail.getSessionId(), detail.getTokenValue());
if (front_flag) {
return "redirect:" + front_url + PaasConstant.PRE_FIX + detail.getTokenValue();
} else {
model.addAttribute("token", detail.getTokenValue());
return "index";
}
}
四、啟動程式
1. 啟動mysql:建立資料庫,執行指令碼。
注:指令碼參考文末程式碼
2. 啟動程式. Oauth2ServerApp -> ClientApp
3. 呼叫客戶端
在瀏覽器位址列輸入:http://127.0.0.1:8081/client
統一跳轉到認證伺服器 http://127.0.0.1:8086/sso,如圖
注:賬套為本文自定義資訊,可酌情擴充套件,也可刪除。
使用者名稱:admin 、密碼:123456
登入成功:
以上,登入成功。
注:如果使用前後端分離系統,請在配置中增加前端首頁地址,如圖
FRONT_URL: http://10.0.0.32:9527/#/
登入成功後,將token返回前端即可。
五、坑
俗話說 “人生到處都是坑,前人栽樹後人乘涼”, 這裡說一下以下幾處問題:
1. 登入頁面,自定屬性如何獲取?
網上有很多實現的例子,比如使用自定義校驗類繼承 WebAuthenticationDetails 等...... 但是此類方法依賴架構較多,對程式碼侵入較大。
本文采用取巧方式,即採用與username拼串方式帶回後臺,處理較簡單,如上面圖中的賬套資訊。
2. 客戶端如何實現jwt校驗?
Client如何校驗自己的token有效性,此處要轉換一下概念,客戶端提供的介面,也是一種資源,所以要將Client端當做resourceServer來處理,這樣一切就合理了。
3. WebSecurityConfigurerAdapter與ResourceServerConfigurerAdapter過濾優先順序?
此問題網上回答較多,比如改order(100)順序等,但實現並不完善。
本文采用國外網友的做法對兩個攔截器過濾進行互斥處理,參考如下
@Override
protected void configure(HttpSecurity http) throws Exception {
RequestMatcher nonResoures = new NegatedRequestMatcher(resources);
http.requestMatcher(nonResoures).authorizeRequests()
.anyRequest().authenticated()
詳細處理方式,可參考文末原始碼。
本文原始碼地址:
https://gitee.com/zetor2020/ym-paas-sso-oauth2.git
下載程式碼的朋友點下star,多謝支援
相關文章
- OAuth2實現單點登入SSOOAuth
- 基於Spring Security Oauth2的SSO單點登入+JWT許可權控制實踐SpringOAuthJWT
- Casdoor + OAuth 實現單點登入 SSOOAuth
- Java架構-spring+springmvc+Interceptor+jwt+redis實現sso單點登入Java架構SpringMVCJWTRedis
- SSO 單點登入
- SSO單點登入
- 單點登入(SSO)
- 記一次 SSO 單點登入實現
- 實戰模擬│單點登入 SSO 的實現
- Spring Cloud Security:Oauth2實現單點登入SpringCloudOAuth
- 談談SSO單點登入的設計實現
- CAS實現單點登入SSO執行原理探究
- JEECG 單點登入 SSO
- 初探單點登入 SSO
- CAS單點登入(SSO)實戰(一)
- SSO單點登入邏輯
- 手把手帶你使用JWT實現單點登入JWT
- 什麼是單點登入(SSO)
- Spring Security OAuth2 單點登入SpringOAuth
- CAS SSO單點登入框架學習框架
- CAS實現單點登入SSO執行原理探究(終於明白了)
- 如何自己實現一個健壯的 SSO 單點登入系統
- SpringCloud微服務實戰——搭建企業級開發框架(四十):使用Spring Security OAuth2實現單點登入(SSO)系統SpringGCCloud微服務框架OAuth
- jwt以及如何使用jwt實現登入JWT
- oauth2.0實現sso單點登入的方式和相關程式碼OAuth
- 基於IdentityServer4的OIDC實現單點登入(SSO)原理簡析IDEServer
- SSO單點登入可以自己實現嗎?--開源軟體誕生10
- 14、sso單點登陸
- [譯] 學習 Spring Security(八):使用 Spring Security OAuth2 實現單點登入SpringOAuth
- 不務正業的前端之SSO(單點登入)實踐前端
- 單點登入SSO和Oauth2.0 文章3OAuth
- 網站登入JWT的實現網站JWT
- CAS SSO單點登入服務端環境搭建服務端
- CAS SSO單點登入客戶端環境搭建客戶端
- 跨域分散式系統單點登入的實現(CAS單點登入)跨域分散式
- JWT實現登入認證例項JWT
- Spring+ Spring cloud + SSO單點登入應用認證SpringCloud
- SSO單點登入最全詳解(圖文全面總結)