springboot springsecurity接入cas单点登录,前后端分离

前言:

前后端是基于若依的前后端分离框架(RuoYi-Vue)进行搭建的,现在需要加入cas单点认证,并且支持配置文件配置的方式,动态的切换认证的方式。
cas服务器的搭建,直接在网上下载即可,本文主要是对cas客户端系统前后端项目的改造,有不当之处,希望大家指正。
特别注意,前端和后端的 casEnable配置需要一致

一 后端

1 在pom中加入依赖

        >
            >org.springframework.security>
            >spring-security-cas>
            >5.2.2.RELEASE>
        >

2 修改LoginUser.java

由于CAS认证需要authorities属性,此属性不能为空,此处为了方便直接new HashSet():

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

2 在application.yml添加配置

其中配置了cas的开关
springboot springsecurity接入cas单点登录,前后端分离_第1张图片

springboot springsecurity接入cas单点登录,前后端分离_第2张图片

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的冲突

二 前端

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

1 在setting.js文件添加配置开关

springboot springsecurity接入cas单点登录,前后端分离_第3张图片

2 修改Navbar.vue中logout方法

springboot springsecurity接入cas单点登录,前后端分离_第4张图片

3 修改permission.js文件全局路由

springboot springsecurity接入cas单点登录,前后端分离_第5张图片
springboot springsecurity接入cas单点登录,前后端分离_第6张图片

springboot springsecurity接入cas单点登录,前后端分离_第7张图片

4 修改request.js文件,添加600状态码

springboot springsecurity接入cas单点登录,前后端分离_第8张图片
至此配置就结束啦

你可能感兴趣的:(java,spring,boot,java,后端)