一、使用步骤
1.引入依赖 基于spring boot版本 2.3.1.RELEASE
<!-- security 对CAS支持 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
<version>4.2.3.RELEASE</version>
</dependency>
2.修改配置 WebSecurityConfigurerAdapter的继承类
实现 UserDetailsService
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.validation.Assertion;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.stereotype.Component;
import top.h2v.vev.app.system.entity.TableRole;
import top.h2v.vev.app.system.entity.TableUser;
import top.h2v.vev.app.system.entity.User;
import top.h2v.vev.app.system.mapper.TableRoleMapper;
import top.h2v.vev.app.system.mapper.TableUserMapper;
import top.h2v.vev.app.system.mapper.UserMapper;
@Component
public class CustomUserServiceImpl implements UserDetailsService, AuthenticationUserDetailsService<CasAssertionAuthenticationToken> {
/**
* 登陆验证时,通过username获取用户的所有权限信息
* 并返回UserDetails放到spring的全局缓存SecurityContextHolder中,以供授权器使用
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (username == null || username.equals("")) {
throw new UsernameNotFoundException("未输入账号");
}
// ... 自己实现 账号拿到了到数据库查一下、该抛异常抛异常,改返回用户信息返回用户信息。
return null;
}
/**
* 这个东西只有在前后台在一堆才能用到,前后分离项目不能这么搞,前台访问后台未登录接口重定向302无法跳转单点系统页面,只能通过反向代理来处理,这个又必须要实现,实现启动报错。
*/
@Override
public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) throws UsernameNotFoundException {
Assertion assertion = token.getAssertion();
if (assertion != null) {
AttributePrincipal principal = assertion.getPrincipal();
if (principal != null) {
Map<String, Object> attributes = principal.getAttributes();
if (attributes != null) {
System.out.println(attributes);
}
}
}
// ... 自己实现 账号拿到了到数据库查一下、该抛异常抛异常,改返回用户信息返回用户信息。
return null;
}
}
我的配置(示例):只看带有 Cas 关键词部分,其他是我需要的业务配置请直接忽略。
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.validation.Cas20ServiceTicketValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.authentication.switchuser.SwitchUserFilter;
@Configuration
@EnableWebSecurity // 启用web权限
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法验证
public class CasSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CasProperties casProperties;// Cas 配置地址
@Autowired
private CustomUserServiceImpl userDetailsService;
@Autowired
private MyAuthenticationFailHandler myFailHandler;
@Autowired
private MyAuthenticationSuccessHandler mySuccessHandler;
/***注入自定义的CustomPermissionEvaluator*/
@Bean
public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler() {
DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
handler.setPermissionEvaluator(new CustomPermissionEvaluator());
return handler;
}
@Bean
public SwitchUserFilter switchUserFilter(CustomUserServiceImpl customUserService) throws Exception {
SwitchUserFilter switchUserFilter = new SwitchUserFilter();
switchUserFilter.setUserDetailsService(customUserService);
switchUserFilter.setSuccessHandler(mySuccessHandler);
switchUserFilter.setFailureHandler(myFailHandler);
return switchUserFilter;
}
/** 定义认证用户信息获取来源,密码校验规则等 */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
auth.authenticationProvider(casAuthenticationProvider());// Cas 注入
}
/** 定义安全策略 */
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()// 配置安全策略
.antMatchers(
"/user/ticketLogin"
).permitAll()// Cas 校验接口
.antMatchers("/user/getUser").authenticated()
.antMatchers("/login/impersonate").hasAuthority("super_admin")
.antMatchers("/logout/impersonate").hasAuthority(SwitchUserFilter.ROLE_PREVIOUS_ADMINISTRATOR)
.anyRequest().authenticated()// 其余的所有请求都需要验证
.and()
.logout()
.permitAll()// 定义logout不需要验证
.and()
.formLogin()// 使用form表单登录
.permitAll();
http.exceptionHandling()
.authenticationEntryPoint(casAuthenticationEntryPoint())// Cas 认证端点
.and()
.addFilter(casAuthenticationFilter())// Cas 认证过滤器
.addFilterAfter(switchUserFilter(userDetailsService), FilterSecurityInterceptor.class)
.addFilterBefore(casLogoutFilter(), LogoutFilter.class)// Cas 登出
.addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class);// Cas 退出
http.csrf().disable(); //禁用CSRF
http.cors().disable();//禁用cors
http.sessionManagement().maximumSessions(1); // 配置了这个 一个账号只能登陆一次
}
@Override
public void configure(WebSecurity web) {
//对于在header里面增加token等类似情况,放行所有OPTIONS请求。
web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
}
/** Cas 认证端点 */
@Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();
casAuthenticationEntryPoint.setLoginUrl(casProperties.getCasServerLoginUrl());
casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
return casAuthenticationEntryPoint;
}
/** Cas 指定service相关信息 */
@Bean
public ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService(casProperties.getAppServerUrl() + casProperties.getAppLoginUrl());
serviceProperties.setAuthenticateAllArtifacts(true);
return serviceProperties;
}
/** Cas 认证过滤器 */
@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
casAuthenticationFilter.setAuthenticationManager(authenticationManager());
casAuthenticationFilter.setFilterProcessesUrl(casProperties.getAppLoginUrl());
return casAuthenticationFilter;
}
/** Cas 认证 Provider */
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
casAuthenticationProvider.setAuthenticationUserDetailsService(userDetailsService);
casAuthenticationProvider.setServiceProperties(serviceProperties());
casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
casAuthenticationProvider.setKey("casAuthenticationProviderKey");
return casAuthenticationProvider;
}
@Bean
public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
return new Cas20ServiceTicketValidator(casProperties.getCasServerUrl());
}
/** Cas 单点登出过滤器 */
@Bean
public SingleSignOutFilter singleSignOutFilter() {
SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
singleSignOutFilter.setCasServerUrlPrefix(casProperties.getCasServerUrl());
singleSignOutFilter.setIgnoreInitConfiguration(true);
return singleSignOutFilter;
}
/** Cas 请求单点退出过滤器 */
@Bean
public LogoutFilter casLogoutFilter() {
LogoutFilter logoutFilter = new LogoutFilter(casProperties.getCasServerLogoutUrl(), new SecurityContextLogoutHandler());
logoutFilter.setFilterProcessesUrl(casProperties.getAppLogoutUrl());
return logoutFilter;
}
}
3.配置 CasProperties
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import lombok.Data;
@Data
@Component
public class CasProperties {
@Value("${cas.server.host.url}")
private String casServerUrl;
@Value("${cas.server.host.login_url}")
private String casServerLoginUrl;
@Value("${cas.server.host.logout_url}")
private String casServerLogoutUrl;
@Value("${app.server.host.url}")
private String appServerUrl;
@Value("${app.login.url}")
private String appLoginUrl;
@Value("${app.logout.url}")
private String appLogoutUrl;
}
application.yml 配置
# CAS服务地址
cas:
server:
host:
url: ${CAS_SERVER_HOST}
# CAS服务登录地址
login_url: ${cas.server.host.url}/login
# CAS服务登出地址
logout_url: ${cas.server.host.url}/logout?service=${app.server.host.url}
# 应用访问地址
app:
server:
host:
url: ${APP_SERVER_HOST}
# 应用登录地址, vue前台需要有对应的页面 不然就是404
login:
url: /#/login
# 应用登出地址
logout:
url: /logout
4.Controller中增加 ticketLogin 接口
@Resource
private CasProperties casProperties;
@Resource
public CustomUserServiceImpl userDetailsService; // 这个东西 就是个传入账号查用户信息的接口,可以自己写
@RequestMapping("ticketLogin")
@ResponseBody
public Object ticketLogin(String ticket) {
TicketValidator ticketValidator = new Cas20ProxyTicketValidator(casProperties.getCasServerUrl());
Assertion casAssertion = null;
try {
casAssertion = ticketValidator.validate(ticket, casProperties.getAppServerUrl() + casProperties.getAppLoginUrl());
if (casAssertion != null) {
AttributePrincipal principal = casAssertion.getPrincipal();
if (principal != null) {
Map<String, Object> attributes = principal.getAttributes();
if (attributes != null) {
System.out.println(attributes);
// 自己实现 业务处理下单点获取的用户信息,比如加个自动注册用户之类的。
}
// 创建登陆用户权限信息
UserDetails userDetails = userDetailsService.loadUserByUsername(principal.getName());
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
// 返回用户信息
return SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
5.前端login登录页跳转处理
<template>
<div></div>
</template>
<script>
import Vue from "vue";
import store from "@/store";
import request from "@/utils/request.js";
import { setToken } from "@/utils/auth"; // cookie工具
export default {
name: "login",
data() {
return {
loading: null
};
},
created() {
this.loading = this.$loading({
lock: true,
text: "正在跳转...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)",
});
if (this.$route.query.ticket) {
request
.get("/user/ticketLogin?ticket=" + this.$route.query.ticket)
.then((rsp) => {
console.log(rsp);
if (rsp.username && rsp.authorities && rsp.authorities.length > 0) {
// 存储用户信息
// 跳转到自己的首页
this.$router.push("/view/");
} else {
// 从后台获取的到用户没有权限。
this.$router.push("/login");
this.loading.close();
this.$alert('账号异常,请联系管理员!', '登录错误', {
type: 'warning',
center: true,
showClose: false,
showConfirmButton: false
});
}
});
} else {
request.get("/user/").then((rsp) => {
if (rsp) {
this.$router.push('/view/' + 1);
} else {
window.location.href = "home";
}
}).catch((e) => {
window.location.href = "home";
});
}
},
mounted() {},
destroyed() {
this.loading.close();
}
};
</script>
vue.config 前台本地反向代理配置
devServer: {
port: 8900, // 端口号
host: ‘0.0.0.0’,
disableHostCheck: true,
https: false,
proxy: {
[‘/’ + process.env.VUE_APP_BASE_API]: {
target: ‘http://127.0.0.1:8600’,
// target: ‘http://10.255.0.21:8022/api’,
changeOrigin: true,
pathRewrite: {
[‘^/’ + process.env.VUE_APP_BASE_API]: ‘’
}
},
‘/home’: {
target: ‘http://127.0.0.1:8600/user/’,
changeOrigin: true,
pathRewrite: {
‘/home’: ‘’
}
},
‘/logout’: {
target: ‘http://127.0.0.1:8600/logout’,
changeOrigin: true,
pathRewrite: {
‘/logout’: ‘’
}
}
}
},
6.nginx 反向代理配置
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 8900;
server_name sso.jzxtech.com;
location = /index.html {
add_header Cache-Control "no-cache, no-store";
}
location /home/ {
proxy_pass http://127.0.0.1:8600/user/;
proxy_cookie_path /home/ /home-api/;
proxy_redirect http://$host:$server_port/ $scheme://$host:$server_port/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /logout {
proxy_pass http://127.0.0.1:8600/logout;
proxy_cookie_path /logout/ /logout-api/;
proxy_redirect http://$host:$server_port/ $scheme://$host:$server_port/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /api/ {
proxy_pass http://127.0.0.1:8600/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
}
总结
前后分离项目常见的跨域问题,可以通过反向代理来处理,以上只是个人配置流程,不建议复制使用,仅供参考,因为部分代码删了,跑不起来的。