前后端分离+springboot+springsecurity单点登录sso实现(csrf过滤器post验证)

springsecurity的单点登录实现起来很容易,但是对csrf的过滤器拦截卡壳了三天,现在对这个测试Demo内容整理,希望帮助到遇到同样问题的同学们!

现在开始讲解:

一共三个项目,认证服务器A、第三方平台B、第三方平台C。下面分别进行说明

一、认证服务器A

先用maven构建好一个基本项目,然后进行开发;

目录结构如下

pom引用主要加入以下四个依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>
编写安全配置服务器

import com.security.sso.authorization.csrfHeader.CsrfHeaderFilter;
import com.security.sso.baseUtils.properties.SecurityProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.csrf.CsrfFilter;
 
@Configuration
public class SsoSecurityConfig extends WebSecurityConfigurerAdapter {
 
    private final static Logger logger = LoggerFactory.getLogger(SsoSecurityConfig.class);
 
    @Autowired
    private UserDetailsService userDetailsService;
 
    @Autowired
    private PasswordEncoder passwordEncoder;
 
    @Autowired
    private SecurityProperties securityProperties;
 
    @Autowired
    protected AuthenticationFailureHandler cstAuthenticationFailureHandler;
 
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        logger.info("用自己定义的usersevices来验证用户");
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    }
 
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //把登录方式改成表单登录的形式
        logger.info("-----定义表单登录方式+自定义成功跳转方法+自定义登录页面----");
        /**/
        http    //.csrf().disable()先禁用跨站访问功能
                .formLogin()//表单登录
                .loginPage("/authentication/require")//自定义登录跳转方法
                .loginProcessingUrl("/authentication/form")// 提交登录表单地址(与登录页中提交的地址一致,就可以提交到登录验证服务MyUserDetailsService 中)
                //.successHandler(cstAuthenticationSuccessHandler)//成功后跳转自定义方法
                .failureHandler(cstAuthenticationFailureHandler)//失败后跳转自定义方法
                .and()
                .httpBasic()
                .and()
                .authorizeRequests()
                .antMatchers("/authentication/require",securityProperties.getBrowser().getSignInPage()).permitAll()//这个页面不需要身份认证,其他都需要
                .anyRequest()//任何请求
                .authenticated()//都需要身份认证
                .and()
                .addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class);//把CSRFtoken设定到cookie
        /**/
        //http.formLogin().and().authorizeRequests().anyRequest().authenticated();
    }
}
值得注意的是最后一行

.addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class);//把CSRFtoken设定到cookie
因为项目采取前后端分离的开发模式,导致session功能失效,在springsecurity4以后默认开启csrf验证,来对应csrf的攻击方式,关于什么是csrf可以通过百度了解,springsecurity配置了相关过滤器,拦截post请求,为了让post请求可以通过验证,我自定义了一个专门为了cookie生成csrf_token传到前台的过滤器,在提交post请求的时候,利用js获取了cookie中从后台生成的csrf_token的值,作为参数传递到请求中,有了这个参数,就可以通过springsecurity的csrf过滤器的验证。这种情况后续再做详细说明,毕竟卡壳两天,身心俱疲。

配置认证服务器

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
 
//认证服务器
@Configuration
@EnableAuthorizationServer
public class SsoAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
 
    private final static Logger logger = LoggerFactory.getLogger(SsoAuthorizationServerConfig.class);
 
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        logger.info("创建两个客户端,为这两个客户端发送授权,或者通过配置文件配置");
        clients.inMemory()
                .withClient("appa")
                .secret("appa_ret")
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .scopes("all")
                .and()
                .withClient("appb")
                .secret("appb_ret")
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .scopes("all");
    }
    /** JWT令牌配置有关的两个 bean **/
    @Bean
    public TokenStore jwtTokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
 
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("ssodemo");//tokenKey
        logger.info("jwt的秘钥是:ssodemo");
        return converter;
    }
 
    //生成令牌
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(jwtTokenStore()).accessTokenConverter(jwtAccessTokenConverter());
    }
 
 
    //安全配置
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //要访问授权服务器的tokenKey(签名秘钥)时,要经过身份认证
        //默认秘钥是无法访问的,这样设置后,只要经过身份认证后,就可以拿到秘钥
        security.tokenKeyAccess("isAuthenticated()");
    }
我在认证服务器定义了两台第三方服务器,就是文章开始所说的B 和 C 配置了他们的名称和密码和权限。

关于生成JWT令牌的代码部分,重点关注密钥的设置,因为第三方服务器会通过拿去密钥来解析令牌的内容,所以密钥是很重要的,一定要注意密钥的安全性。

最关键的就是这以上两个类,如果是自定义登录页面的话,还有一些其他配置,接着说

跳转登录Controller
@RestController
public class JumpToLoginPageController {
 
    private Logger logger = LoggerFactory.getLogger(getClass());
 
    private RequestCache requestCache = new HttpSessionRequestCache();
 
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
 
    @Autowired
    private SecurityProperties securityProperties;
 
    @RequestMapping("/authentication/require")
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {SavedRequest savedRequest = requestCache.getRequest(request, response);
        redirectStrategy.sendRedirect(request,response, securityProperties.getBrowser().getSignInPage());
        return new SimpleResponse("访问的服务需要身份认证,请引导用户到登录页",securityProperties.getBrowser().getSignInPage());
    }
 
}
/**登录失败后的跳转*/
@Component("cstAuthenticationFailureHandler")   //spring security 默认处理器
public class CstAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { //implements AuthenticationFailureHandler
 
    private final static Logger logger = LoggerFactory.getLogger(CstAuthenticationFailureHandler.class);
 
    @Autowired
    private ObjectMapper objectMapper;
 
    @Autowired
    private SecurityProperties securityProperties;
 
    //登录失败有很多原因,对应不同的异常
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException e) throws IOException, ServletException {
        logger.info("登录失败");
 
        //如果自定义设定返回的是JSON格式内容,就把内容返回到前台即可
        if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())){
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());//服务器内部异常
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(e));
        }else{
            super.onAuthenticationFailure(request,response,e);
        }
    }
}
用户service,用于登录用户查询和权限查询必须实现接口 UserDetailsService, SocialUserDetailsService 的方法

loadUserByUsername
import com.security.sso.userPart.entity.*;
import com.security.sso.userPart.service.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.social.security.SocialUserDetails;
import org.springframework.social.security.SocialUserDetailsService;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
 
@Component
public class MyUserDetailsService implements UserDetailsService, SocialUserDetailsService {
 
    private final static Logger logger = LoggerFactory.getLogger(MyUserDetailsService.class);
 
    /********注入自定义的用户service********/
    @Autowired
    private final SysUserService sysUserService;
    @Autowired
    private SysPermissionService sysPermissionService;
    @Autowired
    private SysRoleService sysRoleService;
    @Autowired
    private SysRoleUserService sysRoleUserService;
    @Autowired
    private SysPermissionRoleService sysPermissionRoleService;
 
    @Autowired
    MyUserDetailsService(SysUserService sysUserService){
        this.sysUserService = sysUserService;
    }
 
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //表单登录
        Sys_User sysUser = sysUserService.selectByUsername(username);
        if (sysUser == null) {
            throw new UsernameNotFoundException("用户不存在!");
            //返回方式二:返回带失败原因的数据对象
            //return new User(username, userEntity.getPassword(), true,true,true,true,
            //        AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
        }else{
            logger.info("用户存在,用户:" + username);
            //把用户的角色赋给该用户当作该用户的权限
            List<Sys_Role_User> sruList = sysRoleUserService.selectByUser_id(sysUser.getId());
            List<GrantedAuthority> grantedAuthorities = new ArrayList <>();
            for(Sys_Role_User ru : sruList){
                Sys_Role role = sysRoleService.selectRoleById(ru.getSys_role_id());
                logger.info(username+"-->role:"+role.getName());
                GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getName());
                //1:此处将权限信息添加到 GrantedAuthority 对象中,在后面进行全权限验证时会使用GrantedAuthority 对象。
                grantedAuthorities.add(grantedAuthority);
            }
            return new User(sysUser.getUsername(), sysUser.getPassword(), grantedAuthorities);
        }
    }
 
 
    @Override
    public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
        return null;
    }
}
再UserDetailsService里登录成功后查询出用户的权限(角色)把这些权限放到该用户的授权信息中,返回

GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getName()); grantedAuthorities.add(grantedAuthority);
用户登录后要自动弹出确认授权的页面,需要用户自己选择是否要确认授权。为了给用户更好的体验,重写了确认授权的页面弹出方式,让该确认页面改为自动提交的方式

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.ModelAndView;
 
import javax.servlet.http.HttpServletRequest;
import java.util.Iterator;
import java.util.Map;
 
/**拷贝自源码**/
/**org.springframework.security.oauth2.provider.endpoint.WhitelabelApprovalEndpoint**/
/**需要授权的时候本来源码要弹出确认授权表单页面,现在改造成**/
@RestController
@SessionAttributes("authorizationRequest")
public class SsoApprovalEndpoint {
 
    private static String CSRF = "<input type='hidden' name='${_csrf.parameterName}' value='${_csrf.token}' />";
    private static String DENIAL = "<form id='denialForm' name='denialForm' action='${path}/oauth/authorize' method='post'><input name='user_oauth_approval' value='false' type='hidden'/>%csrf%<label><input name='deny' value='Deny' type='submit'/></label></form>";
    private static String TEMPLATE = "<html>" +
            "<body>" +
            "<div style='display:none;'>" +  //增加DIV 把HTML内容隐藏
            "<h1>OAuth Approval</h1><p>Do you authorize '${authorizationRequest.clientId}' to access your protected resources?</p><form id='confirmationForm' name='confirmationForm' action='${path}/oauth/authorize' method='post'><input name='user_oauth_approval' value='true' type='hidden'/>%csrf%%scopes%<label><input name='authorize' value='Authorize' type='submit'/></label></form>%denial%" +
            "</div>" +
            "<script>document.getElementById('confirmationForm').submit()</script>" + //白页面--自动提交表单
            "</body>" +
            "</html>

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值