基于 spring-security-cas 的 Cas 协议接入(仅支持Spring Boot)
spring-security-cas 是 Spring Security 对 cas-client-core 客户端的整合,适用于使用 Spring Security 作为权限拦截的系统,基于它可快速的实现统一身份认证平台的集成!
第1步:引入依赖
在项目依赖管理中引入 spring-security-cas 依赖 :
Maven 依赖
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-cas -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
<version>5.7.2</version>
</dependency>
Gradle 依赖
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '2.7.2'
// https://mvnrepository.com/artifact/org.springframework.security/spring-security-cas
implementation group: 'org.springframework.security', name: 'spring-security-cas', version: '5.7.2'
第2步:项目配置
首先新建一个YmlProperties类来读取 application.yml 的相关配置
@Component
@Data
public class YmlProperties {
@Value("${app.server.host.url}")
private String appServerUrl;
@Value("${app.login.url}")
private String appLoginUrl;
@Value("${app.logout.url}")
private String appLogoutUrl;
@Value("${cas.server.host}")
private String casServerUrl;
@Value("${cas.server.login_url}")
private String casServerLoginUrl;
@Value("${cas.server.logout_url}")
private String casServerLogoutUrl;
}
新建UserDetail类封装我们需要的用户信息
注意如果没有相关需求,此类可以不建,直接使用spring security提供的org.springframework.security.core.userdetails.User类。
public class UserDetail extends User implements UserDetails, CredentialsContainer {
private Long userId;
public UserDetail(String username,String password, Collection<? extends GrantedAuthority> authorities){
super(username,password,true,true,true,true,authorities);
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
}
新建 MyUserDetailsService 类实现 AuthenticationUserDetailsService 接口
此处的实现主要是为了给Cas客户端提供用户信息,以及相关的用户权限信息
@Service
public class MyUserDetailsService implements AuthenticationUserDetailsService<CasAssertionAuthenticationToken>{
@Autowired
private UserService userService;
@Autowired
private PermissionMapper permissionMapper;
@Override
public UserDetails loadUserDetails(CasAssertionAuthenticationToken token)
throws UsernameNotFoundException {
User user = userService.findByName(token.getName());
if (Objects.isNull(user)) {
throw new RuntimeException("用户不存在");
}
List<String> roleCodes = permissionMapper.getPermissionByUser(user.getId());
if (CollectionUtils.isEmpty(roleCodes)) {
throw new RuntimeException("账户[" + user.getUsername() + "]未绑定角色");
}
Set<GrantedAuthority> authorities = new HashSet<>();
for (String roleCode : roleCodes) {
authorities.add(new SimpleGrantedAuthority(roleCode));
}
UserDetail userDetail=new UserDetail(user.getUsername(),user.getPassword(),authorities);
userDetail.setUserId(user.getId());
return userDetail;
}
}
新建 SecurityCasConfiguration 类实现 WebSecurityConfigurerAdapter接口
@Slf4j
@EnableWebSecurity
//开启@Secured和@PreAuthorize
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@Configuration
public class SecurityCasConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private YmlProperties ymlProperties;
@Autowired
private MyUserDetailsService myUserDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //配置安全策略
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable()
.logout().permitAll() //logout不需要验证
.and()
.headers().frameOptions().disable()
.and()
.cors()
.and().formLogin(); //使用form表单登录
http.exceptionHandling().authenticationEntryPoint(myCasAuthenticationEntryPoint)
.and()
.addFilter(casAuthenticationFilter())
.addFilterBefore(casLogoutFilter(), LogoutFilter.class)
.addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
auth.authenticationProvider(casAuthenticationProvider());
}
/**
* 主要配置的是ServiceProperties的service属性,它指定的是cas回调的地址
*/
@Bean
public ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService(ymlProperties.getAppServerUrl() + ymlProperties.getAppLoginUrl());
serviceProperties.setSendRenew(false);
serviceProperties.setAuthenticateAllArtifacts(true);
return serviceProperties;
}
/**
* 认证的入口,指定cas服务器的登录地址,指定ServiceProperties(主要是获取回调地址)
*/
@Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();
casAuthenticationEntryPoint
.setLoginUrl(ymlProperties.getCasServerLoginUrl());
casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
return casAuthenticationEntryPoint;
}
@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
casAuthenticationFilter.setServiceProperties(serviceProperties()); casAuthenticationFilter.setFilterProcessesUrl(ymlProperties.getAppLoginUrl());
casAuthenticationFilter.setAuthenticationManager(authenticationManager());
//指定登录成功后跳转页面,也可以使用SavedRequestAwareAuthenticationSuccessHandler
// SavedRequestAwareAuthenticationSuccessHandler handler=new SavedRequestAwareAuthenticationSuccessHandler();
// handler.setRedirectStrategy(new MyRedirectStrategy());
// casAuthenticationFilter.setAuthenticationSuccessHandler(handler);
casAuthenticationFilter.setAuthenticationSuccessHandler(
new SimpleUrlAuthenticationSuccessHandler(
ymlProperties.getAppServerUrl() + "/hello"));
casAuthenticationFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());
return casAuthenticationFilter;
}
// class MyRedirectStrategy extends DefaultRedirectStrategy {
//
// @Override
// public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url)
// throws IOException {
// String redirectUrl = calculateRedirectUrl(request.getContextPath(), url);
// redirectUrl = response.encodeRedirectURL(redirectUrl);
// log.info(redirectUrl);
// if(redirectUrl.startsWith("http://")){
// if(redirectUrl.contains("/app/login")){
// redirectUrl="http://localhost:8080/test/index";
// }
// }
// response.sendRedirect(redirectUrl);
// }
// }
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
casAuthenticationProvider.setServiceProperties(serviceProperties());
casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
casAuthenticationProvider
.setAuthenticationUserDetailsService(myUserDetailsService);
casAuthenticationProvider.setKey("casAuthenticationProviderKey");
return casAuthenticationProvider;
}
/**
* 验证ticker,向cas服务器发送验证请求
*/
@Bean
public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
//转Https请求
// HttpURLConnectionFactory httpURLConnectionFactory=new HttpURLConnectionFactory() {
// @Override
// public HttpURLConnection buildHttpURLConnection(URLConnection urlConnection) {
// SSLContext sslContext=null;
// try {
// sslContext = SSLContext.getInstance("SSL");
// sslContext.init(new KeyManager[]{},new TrustManager[]{new X509TrustManager() {
// @Override
// public void checkClientTrusted(X509Certificate[] x509Certificates, String s)
// throws CertificateException {
//
// }
//
// @Override
// public void checkServerTrusted(X509Certificate[] x509Certificates, String s)
// throws CertificateException {
//
// }
//
// @Override
// public X509Certificate[] getAcceptedIssuers() {
// return new X509Certificate[0];
// }
// }},null);
// SSLSocketFactory socketFactory=sslContext.getSocketFactory();
//
// if(urlConnection instanceof HttpsURLConnection){
// HttpsURLConnection httpsURLConnection=(HttpsURLConnection)urlConnection;
// httpsURLConnection.setSSLSocketFactory(socketFactory);
// httpsURLConnection.setHostnameVerifier((s,l)->true);
// }
//
// return (HttpURLConnection)urlConnection;
//
// }catch (Exception e){
// throw new RuntimeException(e);
// }
// }
// };
Cas20ServiceTicketValidator cas20ServiceTicketValidator = new Cas20ServiceTicketValidator(
ymlProperties.getCasServerUrl());
cas20ServiceTicketValidator.setEncoding("UTF-8");
return cas20ServiceTicketValidator;
}
@Bean
public SessionAuthenticationStrategy sessionAuthenticationStrategy() {
SessionAuthenticationStrategy sessionAuthenticationStrategy = new SessionFixationProtectionStrategy();
return sessionAuthenticationStrategy;
}
/**
* 此过滤器向cas发送登出请求
*/
@Bean
public SingleSignOutFilter singleSignOutFilter() {
SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
singleSignOutFilter.setCasServerUrlPrefix(ymlProperties.getCasServerUrl());
singleSignOutFilter.setIgnoreInitConfiguration(true);
return singleSignOutFilter;
}
/**
* 此过滤器拦截客户端的logout请求,发现logout请求后向cas服务器发送登出请求
*/
@Bean
public LogoutFilter casLogoutFilter() {
LogoutFilter logoutFilter = new LogoutFilter(ymlProperties.getCasServerLogoutUrl(),
new SecurityContextLogoutHandler());
logoutFilter.setFilterProcessesUrl(ymlProperties.getAppLogoutUrl());
return logoutFilter;
}
/**
* 去除@Secured的前缀 "ROLE_"
* @return
*/
@Bean
public GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("");
}
}
新建 HelloController
@Slf4j
@RestController
public class HelloController {
@Secured("security_index")
//@PreAuthorize("hasAnyAuthority('security_index')")
@GetMapping("/hello")
public String hello() {
return "首页";
}
}
浏览器访问 http://localhost:8080/test/login 如果未登录会跳转到CAS服务端登录页。
前后端分离项目遇到的问题
对于前后端分离面临的问题是后端不干涉前端页面跳转,在退出登录后,访问前端页面仍可以访问,前端向后端发送请求后端拦截重定向到CAS服务端地址,但是前端页面跳转失败。虽说页面数据是不会加载,但是这不符合我们希望实现的。
目前想到通过实现AuthenticationEntryPoint并重定向到指定接口,返回页面
CAS服务端跳转地址,前端通过拦截器在每个页面访问时都会向后端指定接口发送请求,如果没有登录就会返回CAS服务端地址,登录后直接放行,具体判断我们通过返回状态码来实现。
新建 HelloController
方法中Result类是自定义的统一返回json格式,我会在下面贴出来。
@Slf4j
@RestController
public class HelloController {
@Autowired
private YmlProperties ymlProperties;
/**
* 适用前后端分离
* 当未登录时重定向到此请求,返回给前端CAS服务器登录地址,通过前端跳转
* @return
*/
@GetMapping("/send")
public Result send() {
String url =
ymlProperties.getCasServerLoginUrl() + "?service=" + ymlProperties.getAppServerUrl()
+ ymlProperties.getAppLoginUrl();
return Result.failed().setCode(444).setData(url).setMessage("未登录").setSuccess(false);
}
/**
* 适用前后端分离
* 当登录成功后返回前端数据
* @return
*/
@GetMapping("/login")
public Result login(){
return Result.success(null,"已登录");
}
}
Result统一返回json格式
public interface IErrorCode {
long getCode();
String getMessage();
}
public enum ResultCode implements IErrorCode {
FAILED(500,"操作失败"),
SUCCESS(200,"操作成功");
private long code;
private String message;
private ResultCode(long code,String message){
this.code=code;
this.message=message;
}
@Override
public long getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}
Result
@Data
@Accessors(chain = true)
public class Result<T> {
private boolean success;
private long code;
private String message;
private T data;
private Result() {
}
private Result(boolean success,long code,String message,T data){
this.success=success;
this.code=code;
this.message=message;
this.data=data;
}
/**
* 成功返回结果
* @param data 获取的数据
*/
public static <T> Result<T> success(T data){
return new Result<T>(true,ResultCode.SUCCESS.getCode(),ResultCode.SUCCESS.getMessage(),data);
}
/**
* 成功返回结果
* @param data 获取的数据
* @param message 提示信息
*/
public static <T> Result<T> success(T data,String message){
return new Result<>(true,ResultCode.SUCCESS.getCode(),message,data);
}
/**
* 失败返回结果
* @param <T>
* @return
*/
public static <T> Result<T> failed(){
return new Result<>(false,ResultCode.FAILED.getCode(),ResultCode.FAILED.getMessage(),null);
}
/**
* 失败返回结果
* @param message 提示信息
*/
public static <T> Result<T> failed(String message){
return new Result<>(false,ResultCode.FAILED.getCode(),message,null);
}
}
新建MyCasAuthenticationEntryPoint类实现AuthenticationEntryPoint
@Component
public class MyCasAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Autowired
private YmlProperties ymlProperties;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendRedirect(ymlProperties.getAppServerUrl()+"/send");
}
}
重写 SecurityCasConfiguration 配置类
@Slf4j
@EnableWebSecurity
//开启@Secured和@PreAuthorize
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@Configuration
public class SecurityCasConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private YmlProperties ymlProperties;
@Autowired
private MyCasAuthenticationEntryPoint myCasAuthenticationEntryPoint;
@Autowired
private MyUserDetailsService myUserDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //配置安全策略
.antMatchers("/login","/send").permitAll()
.anyRequest().authenticated() //所有请求都要验证
.and()
.csrf().disable()
.logout().permitAll() //logout不需要验证
.and()
.headers().frameOptions().disable()
.and()
.cors()
.and().formLogin(); //使用form表单登录
http.exceptionHandling().authenticationEntryPoint(myCasAuthenticationEntryPoint)
.and()
.addFilter(casAuthenticationFilter())
.addFilterBefore(casLogoutFilter(), LogoutFilter.class)
.addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
auth.authenticationProvider(casAuthenticationProvider());
}
/**
* 主要配置的是ServiceProperties的service属性,它指定的是cas回调的地址
*/
@Bean
public ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService(ymlProperties.getAppServerUrl() + ymlProperties.getAppLoginUrl());
serviceProperties.setSendRenew(false);
serviceProperties.setAuthenticateAllArtifacts(true);
return serviceProperties;
}
/**
* 认证的入口,指定cas服务器的登录地址,指定ServiceProperties(主要是获取回调地址)
*/
// @Bean
// public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
// CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();
// casAuthenticationEntryPoint
// .setLoginUrl(casProperties.getCasServerLoginUrl());
// casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
// return casAuthenticationEntryPoint;
// }
@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
casAuthenticationFilter.setServiceProperties(serviceProperties());
casAuthenticationFilter.setFilterProcessesUrl(ymlProperties.getAppLoginUrl());
casAuthenticationFilter.setAuthenticationManager(authenticationManager());
//指定登录成功后跳转页面,也可以使用SavedRequestAwareAuthenticationSuccessHandler
// SavedRequestAwareAuthenticationSuccessHandler handler=new SavedRequestAwareAuthenticationSuccessHandler();
// handler.setRedirectStrategy(new MyRedirectStrategy());
// casAuthenticationFilter.setAuthenticationSuccessHandler(handler);
casAuthenticationFilter.setAuthenticationSuccessHandler(
new SimpleUrlAuthenticationSuccessHandler(
ymlProperties.getAppServerUrl() + "/hello"));
casAuthenticationFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());
return casAuthenticationFilter;
}
// class MyRedirectStrategy extends DefaultRedirectStrategy {
//
// @Override
// public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url)
// throws IOException {
// String redirectUrl = calculateRedirectUrl(request.getContextPath(), url);
// redirectUrl = response.encodeRedirectURL(redirectUrl);
// log.info(redirectUrl);
// if(redirectUrl.startsWith("http://")){
// if(redirectUrl.contains("/app/login")){
// redirectUrl="http://10.11.36.21:8080/test/hello";
// }
// }
// response.sendRedirect(redirectUrl);
// }
// }
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
casAuthenticationProvider.setServiceProperties(serviceProperties());
casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
casAuthenticationProvider
.setAuthenticationUserDetailsService(myUserDetailsService);
casAuthenticationProvider.setKey("casAuthenticationProviderKey");
return casAuthenticationProvider;
}
/**
* 验证ticker,向cas服务器发送验证请求
*/
@Bean
public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
//转Https请求
// HttpURLConnectionFactory httpURLConnectionFactory=new HttpURLConnectionFactory() {
// @Override
// public HttpURLConnection buildHttpURLConnection(URLConnection urlConnection) {
// SSLContext sslContext=null;
// try {
// sslContext = SSLContext.getInstance("SSL");
// sslContext.init(new KeyManager[]{},new TrustManager[]{new X509TrustManager() {
// @Override
// public void checkClientTrusted(X509Certificate[] x509Certificates, String s)
// throws CertificateException {
//
// }
//
// @Override
// public void checkServerTrusted(X509Certificate[] x509Certificates, String s)
// throws CertificateException {
//
// }
//
// @Override
// public X509Certificate[] getAcceptedIssuers() {
// return new X509Certificate[0];
// }
// }},null);
// SSLSocketFactory socketFactory=sslContext.getSocketFactory();
//
// if(urlConnection instanceof HttpsURLConnection){
// HttpsURLConnection httpsURLConnection=(HttpsURLConnection)urlConnection;
// httpsURLConnection.setSSLSocketFactory(socketFactory);
// httpsURLConnection.setHostnameVerifier((s,l)->true);
// }
//
// return (HttpURLConnection)urlConnection;
//
// }catch (Exception e){
// throw new RuntimeException(e);
// }
// }
// };
Cas20ServiceTicketValidator cas20ServiceTicketValidator = new Cas20ServiceTicketValidator(
ymlProperties.getCasServerUrl());
cas20ServiceTicketValidator.setEncoding("UTF-8");
return cas20ServiceTicketValidator;
}
@Bean
public SessionAuthenticationStrategy sessionAuthenticationStrategy() {
SessionAuthenticationStrategy sessionAuthenticationStrategy = new SessionFixationProtectionStrategy();
return sessionAuthenticationStrategy;
}
/**
* 此过滤器向cas发送登出请求
*/
@Bean
public SingleSignOutFilter singleSignOutFilter() {
SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
singleSignOutFilter.setCasServerUrlPrefix(ymlProperties.getCasServerUrl());
singleSignOutFilter.setIgnoreInitConfiguration(true);
return singleSignOutFilter;
}
/**
* 此过滤器拦截客户端的logout请求,发现logout请求后向cas服务器发送登出请求
*/
@Bean
public LogoutFilter casLogoutFilter() {
LogoutFilter logoutFilter = new LogoutFilter(ymlProperties.getCasServerLogoutUrl(),
new SecurityContextLogoutHandler());
logoutFilter.setFilterProcessesUrl(ymlProperties.getAppLogoutUrl());
return logoutFilter;
}
/**
* 取出@Secured的前缀 "ROLE_"
* @return
*/
@Bean
public GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("");
}
}
前端只需要访问/login
请求,登录就会返回200状态码和“已登录”提示信息,未登录后端就会重定向/send请求,并返回444状态码和CAS服务端地址,前端根据444状态码拿到CAS服务端地址并跳转页面。
注意: 未登录时后端会重定向一次,可自己通过修改重定向地址,携带数据给前端。
参考:https://blog.csdn.net/new_ord/article/details/109709181
最后编辑:Jeebiz 更新时间:2024-05-07 20:29