springboot springsecurity アクセス cas シングル サインオン、フロントエンドとバックエンドの分離

序文:

フロントエンドとバックエンドは、RuoYi のフロントエンドとバックエンドの分離フレームワーク (RuoYi-Vue) に基づいて構築されており、cas シングルポイント認証を追加する必要があり、設定ファイルの構成と動的切り替えをサポートしています。認証方法。
cas サーバーの構築はインターネットから直接ダウンロードできます。この記事は主に cas クライアント システムのフロントエンドおよびバックエンド プロジェクトの変換について説明しています。不適切な点があれば修正していただければ幸いです。
特别注意,前端和后端的 casEnable配置需要一致

バックエンド

1 pom に依存関係を追加します

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-cas</artifactId>
            <version>5.2.2.RELEASE</version>
        </dependency>

2 LoginUser.java を変更する

CAS 認証にはauthority属性が必要なため、この属性を空にすることはできません。便宜上、ここでは直接 new HashSet() にします。

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities()
    {
    
    
        return new HashSet();
    }

2 application.yml に設定を追加します

其中配置了cas的开关
ここに画像の説明を挿入

ここに画像の説明を挿入

3 CasProperties 構成クラスを追加します


@Component
public class CasProperties {
    
    

    @Value("${app.server.host.url}")
    private String appServerUrl;

    @Value("${app.server.home.url}")
    private String appServerHomeUrl;

    @Value("${app.login.url}")
    private String appLoginUrl;

    @Value("${app.logout.url}")
    private String appLogoutUrl;

    @Value("${app.key}")
    private String appKey;

    @Value("${app.casEnable}")
    private boolean casEnable;

    @Value("${cas.server.host}")
    private String casServerUrl;


    @Value("${cas.server.login_url}")
    private String casServerLoginUrl;

    @Value("${cas.server.logout_url}")
    private String casServerLogoutUrl;


    public CasProperties() {
    
    
    }

    public String getAppKey() {
    
    
        return appKey;
    }

    public void setAppKey(String appKey) {
    
    
        this.appKey = appKey;
    }

    public String getAppServerHomeUrl() {
    
    
        return appServerHomeUrl;
    }

    public void setAppServerHomeUrl(String appServerHomeUrl) {
    
    
        this.appServerHomeUrl = appServerHomeUrl;
    }

    public String getAppServerUrl() {
    
    
        return appServerUrl;
    }

    public void setAppServerUrl(String appServerUrl) {
    
    
        this.appServerUrl = appServerUrl;
    }

    public String getAppLoginUrl() {
    
    
        return appLoginUrl;
    }

    public void setAppLoginUrl(String appLoginUrl) {
    
    
        this.appLoginUrl = appLoginUrl;
    }

    public String getAppLogoutUrl() {
    
    
        return appLogoutUrl;
    }

    public void setAppLogoutUrl(String appLogoutUrl) {
    
    
        this.appLogoutUrl = appLogoutUrl;
    }

    public String getCasServerUrl() {
    
    
        return casServerUrl;
    }

    public boolean isCasEnable() {
    
    
        return casEnable;
    }

    public void setCasEnable(boolean casEnable) {
    
    
        this.casEnable = casEnable;
    }

    public void setCasServerUrl(String casServerUrl) {
    
    
        this.casServerUrl = casServerUrl;
    }

    public String getCasServerLoginUrl() {
    
    
        return casServerLoginUrl;
    }

    public void setCasServerLoginUrl(String casServerLoginUrl) {
    
    
        this.casServerLoginUrl = casServerLoginUrl;
    }

    public String getCasServerLogoutUrl() {
    
    
        return casServerLogoutUrl;
    }

    public void setCasServerLogoutUrl(String casServerLogoutUrl) {
    
    
        this.casServerLogoutUrl = casServerLogoutUrl;
    }
}

4 ジャンプ用のSysCASControllerを追加

@RestController
@RequestMapping("/cas")
public class SysCASController extends BaseController {
    
    
    @Autowired
    private CasProperties casProperties;

    /**
     * 适用前后端分离
     * 当未登录时重定向到此请求,返回给前端CAS服务器登录地址,通过前端跳转
     *
     * @return
     */
    @GetMapping("/send")
    public AjaxResult send() {
    
    
        String url = casProperties.getCasServerLoginUrl() + "?service=" + casProperties.getAppServerUrl() + casProperties.getAppLoginUrl() + "&key=" + casProperties.getAppKey();
        return AjaxResult.error(600, url);
    }

    /**
     * 适用前后端分离
     * 当登录成功后返回前端数据
     *
     * @return
     */
    @GetMapping("/login")
    public AjaxResult login(HttpServletResponse response) throws IOException {
    
    
        response.sendRedirect(casProperties.getAppServerHomeUrl());
        return AjaxResult.success("成功");
    }
}

5 cas認証失敗クラスCasAuthenticationEntryPointImplを追加

这里是直接重定向到controller的send接口回到首页,可根据业务自定义实现认证失败的逻辑


@Component
public class CasAuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    
    

    @Autowired
    private CasProperties casProperties;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
    
    
        response.sendRedirect(casProperties.getAppServerUrl() + "/send");
    }
}

6 ユーザー認証ロジックの追加

根据自己系统内部的认证方式去自行修改,楼主这里是需要取封装全局的LoginUser对象,大家根据自己的系统去实现即可,楼楼的如下:

@Service
public class CasUserDetailsService implements AuthenticationUserDetailsService<CasAssertionAuthenticationToken> {
    
    

    private static final Logger log = LoggerFactory.getLogger(CasUserDetailsService.class);

    @Autowired
    private ISysUserService userService;

    @Autowired
    private SysPermissionService permissionService;

    @Override
    public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) throws UsernameNotFoundException {
    
    
        String username = token.getName();
        SysUser user = userService.selectUserByUserName(username);
        if (StringUtils.isNull(user)) {
    
    
            log.info("登录用户:{} 不存在.", username);
            throw new ServiceException("登录用户:" + username + " 不存在");
        } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
    
    
            log.info("登录用户:{} 已被删除.", username);
            throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
    
    
            log.info("登录用户:{} 已被停用.", username);
            throw new ServiceException("对不起,您的账号:" + username + " 已停用");
        }

        return createLoginUser(user);
    }

    public UserDetails createLoginUser(SysUser user) {
    
    
        return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
    }

7 SecurityConfig 構成クラスを変更します

通过casEnable确认启用的认证方式

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    
    @Autowired
    private CasProperties casProperties;

    @Autowired
    private CasAuthenticationEntryPointImpl casAuthenticationEntryPoint;

    @Autowired
    private CasUserDetailsService casUserDetailsService;
    /**
     * 自定义用户认证逻辑
     */
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 认证失败处理类
     */
    @Autowired
    private AuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 退出处理类
     */
    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;

    /**
     * token认证过滤器
     */
    @Autowired
    private JwtAuthenticationTokenFilter authenticationTokenFilter;

    /**
     * 跨域过滤器
     */
    @Autowired
    private CorsFilter corsFilter;

    /**
     * 解决 无法直接注入 AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
    
    
        return super.authenticationManagerBean();
    }

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
    
    
        if (!casProperties.isCasEnable()) {
    
    
            httpSecurity
                    // CSRF禁用,因为不使用session
                    .csrf().disable()
                    // 认证失败处理类
                    .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                    // 基于token,所以不需要session
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                    // 过滤请求
                    .authorizeRequests()
                    // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                    .antMatchers("/login", "/register", "/captchaImage", "/refToken").anonymous()
                    .antMatchers(
                            HttpMethod.GET,
                            "/",
                            "/*.html",
                            "/**/*.html",
                            "/**/*.css",
                            "/**/*.js"
                    ).permitAll()
                    .antMatchers("/tool/manual**").authenticated()
                    .antMatchers("/doc.html").anonymous()
                    .antMatchers("/swagger-resources/**").anonymous()
                    .antMatchers("/webjars/**").anonymous()
                    .antMatchers("/*/api-docs").anonymous()
                    .antMatchers("/druid/**").anonymous()
                    // 除上面外的所有请求全部需要鉴权认证
                    .anyRequest().authenticated()
                    .and()
                    .cors().and()
                    .headers().frameOptions().disable();

            httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
            // 添加JWT filter
            httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//            // 添加CORS filter
            httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
            httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
        } else {
    
    
            httpSecurity
                    // CSRF禁用,因为不使用session
                    .csrf().disable()
                    // 过滤请求
                    .authorizeRequests()
                    //
                    .antMatchers("/refToken", "/doc.html").anonymous()
                    .antMatchers(
                            HttpMethod.GET,
                            "/",
                            "/*.html",
                            "/**/*.html",
                            "/**/*.css",
                            "/**/*.js"
                    ).permitAll()
                    .antMatchers("/tool/manual**").authenticated()
                    .antMatchers("/cas/**").permitAll()
                    .antMatchers("/swagger-resources/**").anonymous()
                    .antMatchers("/webjars/**").anonymous()
                    .antMatchers("/*/api-docs").anonymous()
                    .antMatchers("/druid/**").anonymous()
                    // 除上面外的所有请求全部需要鉴权认证
                    .anyRequest().authenticated()
                    .and()
                    .cors().and()
                    .logout().permitAll().and()//logout不需要验证
                    .cors().and()
                    .headers().frameOptions().disable();

            httpSecurity.exceptionHandling()
                    .authenticationEntryPoint(casAuthenticationEntryPoint) //认证失败
                    .and().addFilter(casAuthenticationFilter())
                    .addFilterBefore(authenticationTokenFilter, CasAuthenticationFilter.class)
                    .addFilterBefore(casLogoutFilter(), LogoutFilter.class)
                    .addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class);
            httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
            httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
            httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
            httpSecurity.headers().cacheControl();
        }
    }

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
    
    
        return new BCryptPasswordEncoder();
    }

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        if (!casProperties.isCasEnable()) {
    
    
            auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
        } else {
    
    
            auth.authenticationProvider(casAuthenticationProvider());
        }
    }

    /**
     * 主要配置的是ServiceProperties的service属性,它指定的是cas回调的地址
     */
    @ConditionalOnExpression("${app.casEnable}")
    @Bean
    public ServiceProperties serviceProperties() {
    
    
        ServiceProperties serviceProperties = new ServiceProperties();
        serviceProperties.setService(casProperties.getAppServerUrl() + casProperties.getAppLoginUrl());
        serviceProperties.setSendRenew(false);
        serviceProperties.setAuthenticateAllArtifacts(true);
        return serviceProperties;
    }

    @ConditionalOnExpression("${app.casEnable}")
    @Bean
    public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
    
    
        CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
        casAuthenticationFilter.setServiceProperties(serviceProperties());
        casAuthenticationFilter.setFilterProcessesUrl(casProperties.getAppLoginUrl());
        casAuthenticationFilter.setAuthenticationManager(authenticationManager());

        casAuthenticationFilter.setAuthenticationSuccessHandler(
                new SimpleUrlAuthenticationSuccessHandler(
                        casProperties.getAppServerUrl() + "/hello"));
        casAuthenticationFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());
        return casAuthenticationFilter;
    }

    @ConditionalOnExpression("${app.casEnable}")
    @Bean
    public CasAuthenticationProvider casAuthenticationProvider() {
    
    
        CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
        casAuthenticationProvider.setServiceProperties(serviceProperties());
        casAuthenticationProvider.setTicketValidator(cas30ServiceTicketValidator());
        casAuthenticationProvider
                .setAuthenticationUserDetailsService(casUserDetailsService);
        casAuthenticationProvider.setKey("casAuthenticationProviderKey");

        return casAuthenticationProvider;
    }


    /**
     * 验证ticker,向cas服务器发送验证请求
     */
    @ConditionalOnExpression("${app.casEnable}")
    @Bean
    public Cas30ProxyTicketValidator cas30ServiceTicketValidator() {
    
    

        Cas30ProxyTicketValidator cas30ServiceTicketValidator = new Cas30ProxyTicketValidator(
                casProperties.getCasServerUrl());
        cas30ServiceTicketValidator.setEncoding("UTF-8");
        return cas30ServiceTicketValidator;
    }


    @ConditionalOnExpression("${app.casEnable}")
    @Bean
    public SessionAuthenticationStrategy sessionAuthenticationStrategy() {
    
    
        return new SessionFixationProtectionStrategy();
    }


    /**
     * 此过滤器向cas发送登出请求
     */
    @ConditionalOnExpression("${app.casEnable}")
    @Bean
    public SingleSignOutFilter singleSignOutFilter() {
    
    
        SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
        singleSignOutFilter.setCasServerUrlPrefix(casProperties.getCasServerUrl());
        singleSignOutFilter.setIgnoreInitConfiguration(true);
        return singleSignOutFilter;
    }

    /**
     * 此过滤器拦截客户端的logout请求,发现logout请求后向cas服务器发送登出请求
     */
    @ConditionalOnExpression("${app.casEnable}")
    @Bean
    public LogoutFilter casLogoutFilter() {
    
    
        LogoutFilter logoutFilter = new LogoutFilter(casProperties.getCasServerLogoutUrl(),
                new SecurityContextLogoutHandler());
        logoutFilter.setFilterProcessesUrl(casProperties.getAppLogoutUrl());
        return logoutFilter;
    }

    /**
     * 取出@Secured的前缀 "ROLE_"
     *
     * @return
     */
    @ConditionalOnExpression("${app.casEnable}")
    @Bean
    public GrantedAuthorityDefaults grantedAuthorityDefaults() {
    
    
        return new GrantedAuthorityDefaults("");
    }

从上面的配置可以看出,退出处理和token的认证类沿用之前的认证方式即可
特に注意してください (ルー・ルーはここでつまずき、犬の頭を手動で操作します):
在不同模式下通过@ConditionalOnExpression注解,动态的注入bean防止bean的冲突

2 つのフロントエンド

前端不咋会,望大家指正,相互学习

1 設定スイッチをsetting.jsファイルに追加します。

ここに画像の説明を挿入

2 Navbar.vue のログアウト方法を変更します

ここに画像の説明を挿入

3 Permission.js ファイルのグローバル ルーティングを変更します。

ここに画像の説明を挿入
ここに画像の説明を挿入

ここに画像の説明を挿入

4 request.js ファイルを変更し、ステータス コード 600 を追加します。

ここに画像の説明を挿入
ここまでで設定は終わりです

おすすめ

転載: blog.csdn.net/weixin_47914635/article/details/123126269