黑马程序员-学成在线项目-认证授权笔记(2)

认证,就是判断这个用户身份合法不合法;授权,就是看当前用户有没有访问某一资源的权限。

1.导入认证模块:

拿到一个纯净的springboot工程,基于spring cloud架构,要集成spring security,必须严格按照以下步骤:

1.加入security和oath2两个依赖

只要加入依赖,这个项目就被security管控了。

在auth包下的controller包下定义一个LoginController接口用来测试:

@Slf4j
@RestController
public class LoginController {

    @Autowired
    XcUserMapper userMapper;


    @RequestMapping("/login-success")
    public String loginSuccess() {

        return "登录成功";
    }


    @RequestMapping("/user/{id}")
    public XcUser getuser(@PathVariable("id") String id) {
        XcUser xcUser = userMapper.selectById(id);
        return xcUser;
    }

    @RequestMapping("/r/r1")
    @PreAuthorize("hasAuthority('p1')")//拥有p1权限方可访问
    public String r1() {
        return "访问r1资源";
    }

    @RequestMapping("/r/r2")
    @PreAuthorize("hasAuthority('p2')")//拥有p2权限方可访问
    public String r2() {
        return "访问r2资源";
    }



}

我们在数据库找出一个用户id,使用上述代码中的getuser方法的路径,结果弹出一个登录页面,这就是框架自动生成的。这就是认证。

怎么实现授权?

框架管控你的系统,也会管控你的账号。

@PreAuthorize("hasAuthority('p1')")

加了这个注解,框架就会检查访问该方法的账号是否拥有p1权限。

2.在auth模块的config包下定义一个配置类WebSecurityConfig:

/**
 *  安全管理配置
 */
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    DaoAuthenticationProviderCustom daoAuthenticationProviderCustom;


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(daoAuthenticationProviderCustom);
    }

    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

 /*
    //配置用户信息服务
    @Bean
    public UserDetailsService userDetailsService() {
        //这里配置用户信息,这里暂时使用这种方式将用户存储在内存中
        //后续会注释掉这里,然后从数据库查询
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return manager;
    }

 */

    @Bean
    public PasswordEncoder passwordEncoder() {
        /*
        //密码为明文方式
        return NoOpPasswordEncoder.getInstance();
         */
        //密码为加密方式
       return new BCryptPasswordEncoder();
    }

    //配置安全拦截机制
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/r/**").authenticated()//访问/r开始的请求需要认证通过
                .anyRequest().permitAll()//其它请求全部放行
                .and()
                .formLogin().successForwardUrl("/login-success");//登录成功跳转到/login-success
    }



}

这段代码是一个Spring Security配置类,定义了应用程序的安全管理配置。它通过继承 `WebSecurityConfigurerAdapter` 类,并使用注解和方法覆盖来实现安全配置。下面是对每个部分的详细解释:

@EnableWebSecurity

启用Spring Security的Web安全支持。
 

@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)

启用方法级别的安全性注解,如 `@Secured` 和 `@PreAuthorize`。

@Autowired

DaoAuthenticationProviderCustom daoAuthenticationProviderCustom

自动注入自定义的认证提供者。

在auth工程的config包下定义了一个自定义的认证提供者类:

@Slf4j
@Component
/**
 * 自定义DaoAuthenticationProvider
 * 重新了校验密码的方法,因为我们统一了认证的入口,有一些认证方式不需要校验密码
 */
public class DaoAuthenticationProviderCustom extends DaoAuthenticationProvider {


    @Autowired
    public void setUserDetailsService(UserDetailsService userDetailsService) {
        super.setUserDetailsService(userDetailsService);
    }


    //屏蔽密码对比
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)  {


    }

}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(daoAuthenticationProviderCustom);
}


配置自定义的认证提供者 `daoAuthenticationProviderCustom`。

@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}


配置身份验证认证管理器bean,以便其他组件可以自动注入。

@Bean
public PasswordEncoder passwordEncoder() {
/*
        //密码为明文方式
    return NoOpPasswordEncoder.getInstance();
*/
        //密码为加密方式
    return new BCryptPasswordEncoder();
}


配置一个密码编码器bean,测试时使用明文,后续会改为使用加密方式。数据库中的密码加过密的,用户输入的密码是明文,数据库中保存的是加密之后的。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()
            .antMatchers("/r/**").authenticated() // 访问/r/** 的请求需要认证
            .anyRequest().permitAll() // 其他请求允许所有用户访问
            .and()
            .formLogin().successForwardUrl("/login-success"); // 登录成功跳转到/login-success
}

配置http安全
 

http.authorizeRequests()

 定义哪些URL路径应该被保护,哪些不需要保护。
 

.antMatchers("/r/**").authenticated()

 任何以 `/r/` 开头的请求都需要用户认证。
 

.anyRequest().permitAll()

其他请求允许所有用户访问。
 

http.formLogin().successForwardUrl("/login-success")

配置表单登录,并在成功登录后跳转到 `/login-success`。

Spring Security框架原理见这篇博客:

Spring Security-CSDN博客

想要实现认证授权,除了使用Spring Security框架,还必须使用OAuth2协议,详情见下面这篇博客:

OAuth2协议-CSDN博客
 

2.实现网关认证

从上面那篇博客回来,我们已经测试通过了在content工程中用jwt令牌实现的认证功能,如下图:

如果我们每个微服务都自己实现认证太高麻烦,所以我们使用网关认证,如下图所示:

网关的职责:

1、网站白名单维护

针对不用认证的URL全部放行。

2、校验jwt的合法性。

在gateway工程的config包下定义配置类GatewayAuthFilter:

@Component
@Slf4j
public class GatewayAuthFilter implements GlobalFilter, Ordered {


    //白名单
    private static List<String> whitelist = null;

    static {
        //加载白名单
        try (
                InputStream resourceAsStream = GatewayAuthFilter.class.getResourceAsStream("/security-whitelist.properties");
        ) {
            Properties properties = new Properties();
            properties.load(resourceAsStream);
            Set<String> strings = properties.stringPropertyNames();
            whitelist= new ArrayList<>(strings);

        } catch (Exception e) {
            log.error("加载/security-whitelist.properties出错:{}",e.getMessage());
            e.printStackTrace();
        }


    }

    @Autowired
    private TokenStore tokenStore;


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //url
        String requestUrl = exchange.getRequest().getPath().value();
        AntPathMatcher pathMatcher = new AntPathMatcher();
        //白名单放行
        for (String url : whitelist) {
            if (pathMatcher.match(url, requestUrl)) {
                return chain.filter(exchange);
            }
        }

        //检查token是否存在
        String token = getToken(exchange);
        if (StringUtils.isBlank(token)) {
            return buildReturnMono("没有认证",exchange);
        }
        //判断是否是有效的token
        OAuth2AccessToken oAuth2AccessToken;
        try {
            oAuth2AccessToken = tokenStore.readAccessToken(token);

            boolean expired = oAuth2AccessToken.isExpired();
            if (expired) {
                return buildReturnMono("认证令牌已过期",exchange);
            }
            return chain.filter(exchange);
        } catch (InvalidTokenException e) {
            log.info("认证令牌无效: {}", token);
            return buildReturnMono("认证令牌无效",exchange);
        }

    }

    /**
     * 获取token
     */
    private String getToken(ServerWebExchange exchange) {
        String tokenStr = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (StringUtils.isBlank(tokenStr)) {
            return null;
        }
        String token = tokenStr.split(" ")[1];
        if (StringUtils.isBlank(token)) {
            return null;
        }
        return token;
    }




    private Mono<Void> buildReturnMono(String error, ServerWebExchange exchange) {
        ServerHttpResponse response = exchange.getResponse();
        String jsonString = JSON.toJSONString(new RestErrorResponse(error));
        byte[] bits = jsonString.getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return response.writeWith(Mono.just(buffer));
    }


    @Override
    public int getOrder() {
        return 0;
    }
}

这是一个名为 `GatewayAuthFilter` 的过滤器类,实现了 Spring Cloud Gateway 中的 `GlobalFilter` 接口和 `Ordered` 接口,用于进行全局的认证和权限控制。

让我们逐步解释这个过滤器的功能和实现:

1.加载白名单:

在静态代码块中,通过读取 `security-whitelist.properties` 配置文件加载了白名单列表。这个列表中的路径是不需要进行认证的,请求会直接通过。

配置文件在gateway工程的resourses包下。
  
2.过滤方法 `filter`:

这个方法是 `GlobalFilter` 接口的实现,用于实际的过滤逻辑。在方法中,首先判断请求路径是否在白名单中,如果在白名单中则直接放行,不进行认证。然后,从请求头中获取 Authorization 字段,提取出其中的 Token。接着,通过 `tokenStore` 判断 Token 是否有效,如果 Token 无效或已过期,则返回相应的错误信息,否则放行请求。

3.获取 Token 方法 `getToken`:

用于从请求头中提取出 Token 字符串。

4.构建返回的 Mono 对象方法 `buildReturnMono`:

用于构建返回给客户端的 Mono 对象,返回的是一个包含错误信息的 JSON 格式的响应体。

5.实现 `Ordered` 接口方法 `getOrder`:

这个方法用于指定过滤器的执行顺序,数字越小,优先级越高。在这里,返回了 0,表示这个过滤器是优先级最高的,会最先执行。

这个过滤器的主要作用是进行请求的认证和权限控制,在请求到达网关后,会通过该过滤器进行认证,然后再将请求转发给具体的服务进行处理。

3.实现用户认证:

Spring Security工作原理在之前一篇博客中已经介绍了:

用户提交账号和密码由DaoAuthenticationProvider调用UserDetailsService的loadUserByUsername()方法获取UserDetails用户信息。

然后:

UserDetailsService之前曾在 auth工程config包下的WebSecurityConfig配置类中以硬编码的形式自定义过,现在将那里的注释掉。

现在在auth工程的service包下的impl包下定义实现类UserDetailsServiceImpl然后用@Component注解,使用重写的loadUserByUsername方法从数据库获得而不是直接硬编码再用@Bean注解。

在方法上使用 @Bean 注解,您可以告诉 Spring 在容器中注册一个新的 bean 实例。

@Component 是一个通用的注解,用于指示一个类是 Spring 管理的组件。当 Spring 扫描到带有 @Component 注解的类时,它会自动创建该类的一个实例并将其注册到应用程序的上下文中。

虽然 @Bean@Component 都可以用来创建 bean,但是它们之间的主要区别在于使用场景。@Bean 通常用于配置类中,而 @Component 则更适合用于普通的类。

@Slf4j
@Component
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    XcUserMapper xcUserMapper;

    @Autowired
    ApplicationContext applicationContext;

    @Autowired
    XcMenuMapper xcMenuMapper;

    /**
     * 根据username账号查询用户信息
     * @param s  账号
     * @return org.springframework.security.core.userdetails.UserDetails
     */
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //定义一个AuthParamsDto类对象,将传入的json转成它
        AuthParamsDto authParamsDto = null;
        try {
            //将认证参数转为AuthParamsDto类型
            authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
        } catch (Exception e) {
            log.info("认证请求不符合项目要求:{}",s);
            throw new RuntimeException("认证请求数据格式不对");
        }

        //认证类型,有密码登录,微信登录等等
        String authType = authParamsDto.getAuthType();

        //根据认证类型从spring容器取出指定的bean
        String beanName = authType+"_authservice";
        AuthService authService = applicationContext.getBean(beanName, AuthService.class);

        //调用统一的execute方法完成认证
        XcUserExDto xcUserExDto = authService.execute(authParamsDto);

        //将返回的用户信息xcUserExt封装为UserDetails类型并返回
        return getUserPrincipal(xcUserExDto);
    }

    /**
     * 查询用户信息
     * 将返回的XcUserExDto类型的用户信息封装为UserDetails类型
     * @param user 用户扩展信息模型类 包含了用户权限列表
     * @return userDetails
     */
    public UserDetails getUserPrincipal(XcUserExDto user){
        String[] authorities ={};

        //根据用户id查询用户权限
        List<XcMenu> xcMenus = xcMenuMapper.selectPermissionByUserId(user.getId());
        if(xcMenus.size()>0){
            List<String> permissions = new ArrayList<>();
            xcMenus.forEach(menu->{
                //拿到用户拥有的权限标识符
                permissions.add(menu.getCode());
            });
            //将permissions转成数组,得到一个权限数组
            authorities = permissions.toArray(new String[0]);
        }

        String password = user.getPassword();
        //为了安全在令牌中不放密码
        user.setPassword(null);
        //将user对象转json
        String userString = JSON.toJSONString(user);
        //创建UserDetails对象
        UserDetails userDetails = User.withUsername(userString).password(password ).authorities(authorities).build();
        return userDetails;
    }

}

这段代码是一个 Spring Security 的用户认证服务实现类,用于根据用户账号(username)进行用户认证和权限获取。下面是代码的主要逻辑:

1. 使用 Lombok 的 `@Slf4j` 注解,简化日志记录器的声明。
2. 声明了一个 `UserDetailsServiceImpl` 类,并标记为 Spring 组件(`@Component`)和 Spring Security 的用户认证服务(`@Service`)。
3. 实现了 `UserDetailsService` 接口,重写了其中的 `loadUserByUsername` 方法,该方法根据传入的账号(username)进行用户认证和权限获取。
4. 在 `loadUserByUsername` 方法中,首先根据传入的认证参数类型获取对应的认证服务 `AuthService`,然后调用统一认证方法 `execute` 完成认证,最终将认证通过的用户信息封装为 `UserDetails` 类型并返回。

统一认证方法 `execute`定义于auth工程的AuthService接口中,使用账号密码认证时定义一个实现类PasswordAuthServiceImpl实现该接口;使用微信扫码认证时再定义一个实现类WxAuthServiceImpl实现该接口。


5. 定义了一个 `getUserPrincipal` 方法,用于将 `XcUserExDto` 类型的用户信息封装为 `UserDetails` 类型,并获取用户的权限信息。
6. 在 `getUserPrincipal` 方法中,根据用户id查询用户的权限信息,将权限信息封装为 `UserDetails` 对象,并返回。

在getUserPrincipal方法中,为何要将用户拓展模型类对象user转成json数据呢?

到此我们基于Spring Security认证流程修改为如下:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值