spring security + vue + nginx 改造 Cas 单点登录登出

一、使用步骤

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";
        }
		
    }
	
}


总结

前后分离项目常见的跨域问题,可以通过反向代理来处理,以上只是个人配置流程,不建议复制使用,仅供参考,因为部分代码删了,跑不起来的。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值