springboot springsecurity access cas single sign-on, front-end and back-end separation

Foreword:

The front-end and back-end are built based on RuoYi's front-end and back-end separation framework (RuoYi-Vue). Now it is necessary to add cas single-point authentication, and it supports configuration file configuration and dynamic switching of authentication methods.
The construction of the cas server can be downloaded directly from the Internet. This article is mainly about the transformation of the front-end and back-end projects of the cas client system. If there is something inappropriate, I hope you can correct me.
特别注意,前端和后端的 casEnable配置需要一致

a backend

1 Add dependencies to pom

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

2 Modify LoginUser.java

Since CAS authentication requires the authorities attribute, this attribute cannot be empty, here for convenience directly new HashSet():

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

2 Add configuration in application.yml

其中配置了cas的开关
insert image description here

insert image description here

3 Add CasProperties configuration class


@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 Added SysCASController for jumping

@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 Add cas authentication failure class 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 Add user authentication logic

根据自己系统内部的认证方式去自行修改,楼主这里是需要取封装全局的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 Modify the SecurityConfig configuration class

通过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的认证类沿用之前的认证方式即可
Pay special attention (Lou Lou stumbled here, manual dog head):
在不同模式下通过@ConditionalOnExpression注解,动态的注入bean防止bean的冲突

Two front ends

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

1 Add a configuration switch to the setting.js file

insert image description here

2 Modify the logout method in Navbar.vue

insert image description here

3 Modify the global routing of the permission.js file

insert image description here
insert image description here

insert image description here

4 Modify the request.js file and add a 600 status code

insert image description here
So far the configuration is over

Guess you like

Origin blog.csdn.net/weixin_47914635/article/details/123126269