前言

1.spring-security是spring官方推荐的【认证、授权】框架

2.本文介绍spring-security在【表单认证】、【jwt认证】、【社交登录】3种场景中的运用

3.RBAC权限模块的代码,将以伪代码形式给出

总体介绍

过滤器链

spring-security使用多个Filter来实现:认证、授权、记住我、成功(失败)跳转、跨域访问、防跨站攻击。。。一系列功能。

 

其中【UsernamePasswordAuthenticationFilter】与【BasicAuthenticationFilter】为核心过滤器(与业务绑定),分别管理【用户名/密码的认证】与【token认证】

认证、授权流程

 

第一步:认证
根据username查询系统中是否存在该用户
第二步:授权
认证成功后,获取该用户的权限,封装为UserDetail对象(上图对于理解spring security整套认证、授权流程至关重要)
PS:根据此图,调试代码,弄明白spring-security整个认证、授权流程,那么这套框架的核心就理解的差不多了

案例

表单认证

1.流程图

 

2.实现

核心配置

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // 允许iframe嵌套,解决:frame because it set 'X-Frame-Options' to 'deny 问题
        http.headers().frameOptions().disable();
        // 放行所有option请求
        http.authorizeRequests()
                .requestMatchers(CorsUtils::isPreFlightRequest)
                .permitAll();

        http.csrf().disable()   // 关闭跨站攻击保护,否则无法进行跨域访问
                .cors()         // 开启跨域支持
                .and().formLogin()
                    .loginPage("/login.html")   // 登录页
                    .loginProcessingUrl("/user/login")   // 登录Action提交url
                    .defaultSuccessUrl("/success.html").permitAll()
                    .failureUrl("/error.html")
//                    .failureHandler(failureHandler)     // 登录失败自定义处理器(会覆盖failureUrl()的设置)
//                    .successHandler(successHandler)     // 登录成功自定义处理器(会覆盖defaultSuccessUrl()的设置)
                .and().logout()   // 退出登录
                    .logoutUrl("/logout")   // 退出Action提交url
                    .addLogoutHandler(definedLogoutHandler) // 退出处理逻辑编写
                .and().exceptionHandling().accessDeniedPage("/unauth.html") // 无权限访问时跳转的html
                // 放开访问权限的url
                .and().authorizeRequests()
                    .antMatchers("/", "/user/login", "/logout", "/user/logout").permitAll()
                    .anyRequest().authenticated();
//                .and().rememberMe()   // 【记住我】功能设置
//                .and().sessionManagement()    // session管理:session有效期、session个数(可实现互踢)

    }
View Code

配置的说明,注释已经说的很详细了,可自行阅读
说明:

a. 跨站攻击【.csrf().disable()】必须要关闭,否则跨域配置【.cors()】不起作用
b. 登录action的url【/user/login】与退出登录action的url【/logout】只需要配置路径即可,不需要自己实现,由spring-security框架自行实现

 跨域配置

    @Bean
    CorsFilter corsFilter() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.addAllowedOrigin(CorsConfiguration.ALL);
        configuration.addAllowedHeader(CorsConfiguration.ALL);
        configuration.addAllowedMethod(CorsConfiguration.ALL);
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return new CorsFilter(source);
    }
View Code

补充

FormLogin模式虽然依托于session,但是仍然可以实现微服务架构下的单点登录

实现步骤:

a. session需要保存在一个公共可访问区域(数据库 or redis)
b. session登录后,需要扩展session的作用域范围

例如:两个服务:www.baidu.com,map.baidu.com

当在www.baidu.com登录成功后,得到的session的作用域为www.baidu.com,此时我们需要修改session的作用域为:baidu.com。
这样,当我们在登录成功的状态下,再访问map.baidu.com时,由于session的作用域为baidu.com,所以session在map.baidu.com下仍然有效,所以登录状态为【已登录】

jwt认证

1.流程图

BasicLogin模式,采用的是http身份认证,属于最简单的认证、授权功能

2.实现

jwt的认证过程,有两步:【登录-生成token】、【验证token】需要将以上两个步骤,封装为Filter,添加到spring-security整个Filter链中

1.登录-生成token

a. 自定义Filter,用此Filter替换掉UsernamePasswordAuthenticationFilter,这样过滤器链的【用户名-密码认证】,用的就是我们自定义的业务逻辑
b. 继承UsernamePasswordAuthenticationFilter,重写【获取用户传递的信息方法(attemptAuthentication)】、【认证成功方法(successfulAuthentication)】、【认证失败方法(unsuccessfulAuthentication)】,实现自定义认证逻辑

核心代码

获取表单数据:spring-security的认证方法,需要将用户输入封装为:UsernamePasswordAuthenticationToken(用户名,密码,权限)

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        //获取表单提交数据
        try {
            User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
            return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),
                    user.getPassword()));
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException();
        }
    }
View Code

认证成功

    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response, FilterChain chain, Authentication authResult) {
        //认证成功,得到认证成功之后用户信息
        SecurityUser user = (SecurityUser) authResult.getPrincipal();
        //根据用户名生成token
        String token = tokenManager.generateToken(user);
        //返回token
        ResponseUtil.out(response, token);
    }
View Code

认证失败

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException failed) {
        ResponseUtil.out(response, "认证失败");
    }
View Code

2.验证token

a. 自定义Filter,用此Filter替换掉BasicAuthenticationFilter。由于jwt依托于http协议,BasicAuthenticationFilter就是针对http的Filter,所以选择替换掉BasicAUthenticationFilter
b. 继承BasicAuthenticationFilter,重写【过滤器方法(doFilterInternal)】实现自定义业务逻辑

核心代码

认证过滤

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //获取当前认证成功用户权限信息
        UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);
        //判断如果有权限信息,放到权限上下文中
        if (authRequest != null) {
            SecurityContextHolder.getContext().setAuthentication(authRequest);
        }
        chain.doFilter(request, response);
    }
View Code

获取用户认证信息

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        //从header获取token
        String token = request.getHeader("token");
        if (token != null) {
            // 用户名
            String username = tokenManager.getUsernameFromToken(token);

            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                if (tokenManager.validateToken(token, userDetails)) {
                    //给使用该JWT令牌的用户进行授权
                    UsernamePasswordAuthenticationToken authenticationToken
                            = new UsernamePasswordAuthenticationToken(userDetails, token,
                            userDetails.getAuthorities());
                    return authenticationToken;
                }
            }
        }
        return null;
    }
View Code

社交登录

1.流程图

a. 引导【client】跳转至【第三方应用-登录】进行【登录-授权】操作,成功后返回【code】
b. 【client】获取到【code】向【第三方应用-认证】换取【access_token】
c. 【client】获取到【access_token】向【第三方应用-资源】发起API请求,获取开放的【用户信息】

2.实现思路

a. 【登录功能】由【第三方平台】实现,登录成功后,需要将返回的信息保存入本地。与【本地用户】建立关联关系
b. 【认证功能】与jwt认证功能类似,只需要做微调

3.实现(本实例使用【新浪微博开放平台】)

1.平台地址:https://open.weibo.com/

2.应用信息

 

 

3.回调信息

 

4.操作步骤

 

 

5.access_token

【uid】用户标志id,需要用它与本地用户建立关联关系
【expires_in】access_token有效日期,生成jwt时有效期需要使用该属性

6.生成token

a. 在【第三方平台】的回调url中,获取到返回的code
b. 使用code,向【第三方平台】换取对应的access_token
c. access_token中的uid与本地用户User,建立对应关系

@RestController
@RequestMapping("/oauth")
public class OAuthController {

    @Autowired
    TokenManager tokenManager;

    @Autowired
    MyUserDetailsService myUserDetailsService;

    // 应该存在redis中
    public static Map<String, Social> socials = new HashMap<>();

    private static final String KEY = "YOU APP ID";
    private static final String SECRET = "YOU SECURITY";

    @GetMapping("/token")
    public String token(@RequestParam String code) {

        // 获取access_token
        String url = "https://api.weibo.com/oauth2/access_token";
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(url)
                // Add query parameter
                .queryParam("client_id", KEY)
                .queryParam("client_secret", SECRET)
                .queryParam("grant_type", "authorization_code")
                .queryParam("redirect_uri", "http://192.168.0.14:9730/oauth/token")
                .queryParam("code", code);

        RestTemplate restTemplate = new RestTemplateBuilder().build();
        Social social = restTemplate.postForObject(builder.toUriString(), null, Social.class);
        socials.put(social.getUid(), social);

        // 生成token
        User user = new User("admin", "123", "社交登录id");
        SecurityUser securityUser = new SecurityUser(user, social, null);
        String token = tokenManager.generateToken(securityUser);
        return token;
    }
}
View Code

7.剩余部分

剩余部分与jwt大同小异,此处不再赘述,可自行查看源码

 

结束语:感谢大家的耐心阅读 

代码下载

示例代码下载

 

posted on 2021-05-12 17:26  安静生活  阅读(356)  评论(1编辑  收藏  举报