Spring Security-认证实现

1.已实现内容说明

2.问题

3.原因分析

4.解决方法

4.1.自定义过滤器

4.2.配置过滤器

 5.测试


前几篇文章分别介绍了Security的原理和源码、讲解了实现整合Security的实现思路、讲解了认证成功和失败后的处理方法。但整合并没有完全结束。

1.已实现内容说明

         1.自定了认证过滤器实现UsernamePasswordAuthenticationFilter,重写了里面的认证方法。

@Slf4j
public class CustomerAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;
    private final JwtProperty jwtProperty;

    public CustomerAuthenticationFilter (AuthenticationManager authenticationManager, JwtProperty jwtProperty) {
        this.authenticationManager = authenticationManager;
        this.jwtProperty = jwtProperty;
    }

    /**
     * 重写用户认证入口
     *
     * @param request
     * @param response
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        String contentType = request.getContentType();
        String username;
        String password;
        // json请求处理
        if ("application/json".equals(contentType)) {
            SysUser sysUser = null;
            try {
                sysUser = new ObjectMapper().readValue(request.getInputStream(), SysUser.class);
            } catch (IOException e) {
                log.error("请求数据异常,未读取到用户信息");
                return null;
            }
            username = sysUser.getUsername();
            password = sysUser.getPassword();
        } else {
            //表单数据处理
            username = this.obtainUsername(request);
            password = this.obtainPassword(request);
        }

        //如果用户没有输入用户名返回null
        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
            log.error("请求数据异常,未提交用户名或密码");
            throw new UsernameNotFoundException("请求参数异常");
        }
        return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
    }

    /**
     * 认证成功的处理
     *
     * @param request
     * @param response
     * @param chain
     * @param authResult
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void successfulAuthentication(HttpServletRequest request,
                                         HttpServletResponse response,
                                         FilterChain chain,
                                         Authentication authResult) throws IOException, ServletException {
        SysUser user = new SysUser();
        user.setUsername(authResult.getName());
        user.setAuthoritiesFromAuthority(authResult.getAuthorities());
        String token = JwtUtils.generateExpireTokenWithSecretKey(user, jwtProperty.getBase64EncodedKey(), 24 * 60 * 60);
        response.addHeader("Authorization", "Bearer " + token);
        //登录成功时,返回json格式进行提示
        response.setContentType("application/json;charset=utf-8");
        response.setStatus(HttpServletResponse.SC_OK);
        PrintWriter out = response.getWriter();
        out.write(new ObjectMapper().writeValueAsString(R.ok(null, "登陆成功")));
        out.flush();
        out.close();
    }

    /**
     * 认证失败异常处理
     *
     * @param request
     * @param response
     * @param failed
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void unsuccessfulAuthentication(HttpServletRequest request,
                                           HttpServletResponse response,
                                           AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        R r = R.failed();
        if (failed instanceof InsufficientAuthenticationException) {
            r.setMsg("请先登录~~");
        } else if (failed instanceof BadCredentialsException) {
            r.setMsg("用户名或密码错误");
        } else if (failed instanceof UsernameNotFoundException) {
            r.setMsg(failed.getLocalizedMessage());
        } else {
            r.setMsg("用户认证失败,请检查后重试");
        }
        writer.write(new ObjectMapper().writeValueAsString(r));
        writer.flush();
        writer.close();
    }
}

        2.自定义实现了UserDetail对象。

public class EdenUser extends User {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    /**
     * 用户ID
     */
    private String userId;

    /**
     * 拓展字段:权限
     */
    private List<String> perms;

    @JsonCreator
    public EdenUser(@JsonProperty("userId") String userId,
                    @JsonProperty("username") String username, @JsonProperty("password") String password,
                    @JsonProperty("enabled") boolean enabled, @JsonProperty("accountNonExpired") boolean accountNonExpired,
                    @JsonProperty("credentialsNonExpired") boolean credentialsNonExpired,
                    @JsonProperty("accountNonLocked") boolean accountNonLocked,
                    @JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities) {
        super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
        this.userId = userId;
        if (CollectionUtil.isNotEmpty(authorities)) {
            this.perms = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
        }
    }

    @JsonIgnore
    @Override
    public Collection<GrantedAuthority> getAuthorities() {
        if (CollectionUtil.isNotEmpty(perms)) {
            return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.collectionToCommaDelimitedString(perms));
        }
        return null;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return true;
    }
}

        3. 自定义实现并重写了UserDetailsService loadUserByUsername方法,有了自己的从数据库获取用户信息的逻辑。

因为是分布式系统,所以这里发出了Feign调用请求,调用system服务获取用户信息(主要是用户名和密码)、权限和菜单权限。system的代码这里就不再贴了,每个人的表结构也不同。即便你是单体服务直接查库,反正只要把这些信息都拿到,满足最后封装你的UserDetail对象的条件就行了。

@Slf4j
@Service
public class EdenUserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private RemoteUserFeign userFeign;

    /**
     * 自定义授权认证
     *
     * @param username 用户登录时输入的用户名
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = userFeign.loadUserByUsername(username);

        if (sysUser == null) {
            log.error("未查询到用户:{}的账户信息", username);
            throw new UsernameNotFoundException("用户名或密码错误");
        }

        // 封裝並返回對象
        List<String> permissions = sysUser.getRoles();
        permissions.addAll(sysUser.getPerms());
        return new EdenUser(sysUser.getId(), username, sysUser.getPassword(),
                true, true, true, true,
                AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.collectionToCommaDelimitedString(permissions)));
    }
}

        4.增加了Security的配置

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class EdenWebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailService;

    @Autowired
    private EdenLoginSuccessHandler loginSuccessHandler;

    @Autowired
    private EdenLoginFailureHandler loginFailureHandler;

    @Autowired
    private EdenLogoutSuccessHandler logoutSuccessHandler;

    @Autowired
    private AuthProperty authProperty;

    @Autowired
    private JwtProperty jwtProperty;
    ;

    /***
     * 采用BCryptPasswordEncoder对密码进行编码
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 核心过滤器配置:
     * 一般用来配置对静态资源的拦截忽略
     *
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                .antMatchers("/test/**");
    }

    /**
     * 安全过滤器链配置:
     * 用于构建一个安全过滤器链 SecurityFilterChain
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        String ignoreUrls = "";
        if (CollectionUtil.isNotEmpty(authProperty.getIgnoreUrls())) {
            ignoreUrls = String.join(StrUtil.COMMA, authProperty.getIgnoreUrls());
            http.authorizeRequests().antMatchers(ignoreUrls).permitAll();
        }
        http
                // 禁用csrf攻击防护
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                //其余接口,认证通过后即可访问
                .anyRequest().authenticated()
                .and()
                //启用表单身份验证
                .formLogin()
                //设置进行登录请求处理的接口地址
                .loginProcessingUrl("/login")
                .permitAll() // 和表单登录有关的直接放行
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessHandler(logoutSuccessHandler)
                .permitAll() // 和退出登录有关的直接放行
                .and()
                .addFilter(new CustomerAuthenticationFilter(authenticationManager(), jwtProperty))
        ;

    }

    /**
     * 身份认证管理器配置:
     * 配置身份认证相关
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
    }

    /**
     * @Author: Yan
     * @Since: 2023/2/4
     * @Description: AuthenticationManager对象在OAuth2认证服务中要使用,提取放入IOC容器中
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

        5.经过上面的配置,当请求登录通过了认证后,Security如约地返回了Token。Token中是自定义封装的信息,包含有用户名、密码和权限列表都作为payload放在jwt中,而且还被秘钥加了密。

        6.因为是分布式系统,网关(gateway)也配置了Token的验证和放行。如果你不知道我在说什么,看看之前的文章《Spring Security的实现思路》。在这一篇文章里提到了我们所使用的网关验证、资源服务解析的方式。而且网关顺利的拦截到了请求,并验证通过了Token后放行。

@Component
@AllArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    private final JwtProperty jwtProperty;

    private final List<String> IGNORE_URIS = CollectionUtil.newArrayList("/auth/login", "/auth/logout", "/auth/oauth/**");

    @SneakyThrows
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        //直接放行部分请求路径,如登录、退出等  需要排除的路径弄成可yaml配置的
        if (IGNORE_URIS.contains(request.getURI().getPath())) {
            return chain.filter(exchange);
        }

        //获取请求头中的令牌
        String token = request.getHeaders().getFirst(SecurityConstant.AUTHENTICATION_HEADER);
        // 其他路径,没有令牌或令牌校验失败,不允许访问
        if (StrUtil.isBlank(token) || !JwtUtils.verify(token.replace(SecurityConstant.AUTHENTICATION_PREFIX, ""),
                jwtProperty.getBase64EncodedKey())) {
            ServerHttpResponse response = exchange.getResponse();
            // 结束请求并响应信息
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.writeWith(
                    Mono.just(response.bufferFactory()
                            .wrap(new ObjectMapper().writeValueAsBytes(R.failed(HttpStatusEnum.of(HttpStatus.UNAUTHORIZED.value()))))
                    )
            );
        }
        return chain.filter(exchange);
    }

    /**
     * 优先执行对权限的校验
     *
     * @return 最高执行级别
     */
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

上面就是我们为整合Security已经完成的工作。后面我们将请求一个自定义接口,看能够顺利进入接口拿到响应。 


下面是我在认证服务中自定义的一个接口,专门用来做请求的验证。

@RestController
@RequestMapping("/oauth")
public class AuthController {
    /**
     * 登陆后,访问测试
     *
     * @return
     */
    @RequestMapping("/toLogin")
    public String toLogin() {
        return "login";
    }
}

2.问题

如果按照上面贴出的配置,我们能请求到接口嘛?答案是否定的。

Token也携带了,放出请求后,网关放行了。但我还是看到了熟悉的登陆界面.....

这说明我们还是被Security拦住了?why???

3.原因分析

结论是,因为Security始终没有办法从SecurityContext中获取到用户信息。

Security框架获取信息肯定是从他自己的SecurityContext上下文中取,不可能从request请求里面拿吧,即便拿了也只能拿到Jwt形式的Token而已,框架自己又不会解密。他从上下文中得不到你的用户信息认为你就是没有登录,没登录那就去登录页面呗。

我们之前说过Secuirty是通过过滤器链实现功能的。他的过滤器有15个。

第二个过滤器的名字SecurityContextPersistenceFilter,一看就是上下文持久化有关的。当你发出请求并没有将用户信息解析出来放在SecurityContext中的时候,这个过滤器从上下文里面也就拿不到,拿不到就没有办法做持久化,供后续使用。

 zu

4.解决方法

既然原因是没有放用户信息在SecurityContext,那就放一个呗。但,在哪放?

我们可以从上面的代码中看到,这个过滤器执行过程中有放行的操作chain.doFilter(holder.getRequest(), holder.getResponse());放行就是传递给后面的过滤器链继续执行,但代码里我们看到最后会回到finally代码块里面继续执行持久化的操作。

那我们就在持久化之前保证他能从SecurityContext中获取到用户信息,能有东西做持久化不就好了?我们来说具体做法。

4.1.自定义过滤器

在这个过滤器doFilter的时候,把请求放行到你自定义的过滤器;在自定义的过滤器里往SecurityContext放入用户信息就行了。

示例如下:

public class BeforeAuthenticateCheckFilter extends OncePerRequestFilter {

    private JwtProperty jwtProperty;

    public BeforeAuthenticateCheckFilter(JwtProperty jwtProperty) {
        this.jwtProperty = jwtProperty;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        //获取请求头中的令牌
        String token = request.getHeader(SecurityConstant.AUTHENTICATION_HEADER);
        // 其没有令牌,直接放行
        if (StrUtil.isBlank(token)) {
            filterChain.doFilter(request, response);
            return;
        }
        //解析令牌
        SysUser sysUser = JsonUtils.toBean(JwtUtils.parserToken2ObjectWithSecretKey(
                token.replace(SecurityConstant.AUTHENTICATION_PREFIX, ""),
                jwtProperty.getBase64EncodedKey()), SysUser.class);
        if (null == sysUser) {
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            PrintWriter out = response.getWriter();
            out.write(new ObjectMapper().writeValueAsString(R.failed("用户信息异常")));
            out.flush();
            out.close();
        }
        SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(sysUser.getUsername(),
                sysUser.getPassword(),
                AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.collectionToCommaDelimitedString(sysUser.getAuthorities()))));
        filterChain.doFilter(request, response);
    }
}

 特别注意:如果没有请求中没有令牌,一定要放行,让后面的过滤器处理,不要直接结束请求。想想这个道理,后面我们需要把它放在登录认证过滤器的前面,万一人家就是要去登录呢,现在肯定是没有Token的,那岂不是永远也登陆不了了....

        // 其没有令牌,直接放行
        if (StrUtil.isBlank(token)) {
            filterChain.doFilter(request, response);
            return;
        }

4.2.配置过滤器

上面是自定义过滤器,解析出用户信息并存入上下文。还有一步那就是自定义过滤器加入到过滤器链中。

通常的做法是:自定义过滤器加在UsernamePasswordAuthenticationFilter过滤器之前。

示例如下:

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

        String ignoreUrls = "";
        if (CollectionUtil.isNotEmpty(authProperty.getIgnoreUrls())) {
            ignoreUrls = String.join(StrUtil.COMMA, authProperty.getIgnoreUrls());
            http.authorizeRequests().antMatchers(ignoreUrls).permitAll();
        }
        http
                // 此处省略无关配置
                .addFilterBefore(new BeforeAuthenticateCheckFilter(jwtProperty), CustomerAuthenticationFilter.class)
                .addFilter(new CustomerAuthenticationFilter(authenticationManager(), jwtProperty));

    }

CustomerAuthenticationFilter是我们自定义的UsernamePasswordAuthenticationFilter的实现类,用来处理用户信息认证逻辑的核心过滤器。我们这次自定义的过滤器就放在他前面。

 5.测试

重启后再进行测试。持久化之前能取到用户信息了; 接口有响应了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值