【Spring Boot】Spring Boot之整合Spring security实现JWT权限认证

浪子不回头ぞ 提交于 2020-07-27 21:56:04

一、先看下DaoAuthenticationProvider的认证过程

 

1、从读取用户名和密码开始的身份验证Filter将一个UsernamePasswordAuthenticationToken传递给由ProviderManager实现的AuthenticationManager。

2、ProviderManager被配置为使用DaoAuthenticationProvider类型的AuthenticationProvider。

3、第三个DaoAuthenticationProvider从UserDetailsService中查找用户详细信息。

4、DaoAuthenticationProvider使用PasswordEncoder验证上一步返回的用户详细信息上的密码。

5、当身份验证成功时,返回的身份验证类型为UsernamePasswordAuthenticationToken,其主体是已配置的UserDetailsService返回的用户详细信息。最终,返回的UsernamePasswordAuthenticationToken将由身份验证过滤器在securitycontext中设置。

二、过程分析

1)登陆认证
项目中,我们如何实现这个过程

1、自定义UserDetailsService(用于提供用户信息一般通过用户名从数据库查询)

/**
 * 用户登录认证信息查询
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private TUserDAO tUserDAO;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        TUser user = tUserDAO.getByUserName(username);
        if (user == null) {
            throw new UsernameNotFoundException("该用户不存在");
        }
        // 用户权限列表,根据用户拥有的权限标识与如 @PreAuthorize("hasAuthority('sys:menu:view')") 标注的接口对比,决定是否可以调用接口
//        Set<String> permissions = sysUserService.findPermissions(user.getName());
        Set<String> permissions = new HashSet<>();
        permissions.add("query");
        List<GrantedAuthority> grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList());
        return new JwtUserDetails(user.getName(), user.getPassword(), user.getSalt(), user.getAccountType(), grantedAuthorities);
    }
}
View Code

2、自定义DaoAuthenticationProvider (用于验证用户密码是否正确)

/**
 * 身份验证提供者
 */
public class JwtAuthenticationProvider extends DaoAuthenticationProvider {

    public JwtAuthenticationProvider(UserDetailsService userDetailsService) {
        setUserDetailsService(userDetailsService);
    }

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }

        Integer accountType = ((JwtUserDetails) userDetails).getAccountType();

        String presentedPassword = authentication.getCredentials().toString();
        String salt = ((JwtUserDetails) userDetails).getSalt();
        if (!new PasswordEncoder(salt).matches(userDetails.getPassword(), presentedPassword)) {
            logger.debug("Authentication failed: password does not match stored value");
            throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
    }

}
View Code

在spring security中,将用户的获取和密码的验证过程拆开了
代码实现:

@Autowired
private AuthenticationManager authenticationManager;

public
HttpResult login(@RequestBody LoginForm loginForm, HttpServletRequest request) throws IOException { // 1、这一步主要获取用户密码 TUserDO tUser = tUserService.login(loginForm.getUsername()); // 2、进行spring security 认证 JwtAuthenticatioToken token = new JwtAuthenticatioToken(loginForm.getUsername(),loginForm.getPassword()); token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 执行登录认证过程 Authentication authentication = authenticationManager.authenticate(token); // 认证成功存储认证信息到上下文 SecurityContextHolder.getContext().setAuthentication(authentication); // 生成令牌并返回给客户端 token.setToken(JwtTokenUtils.generateToken(authentication)); return HttpResult.success(token); }

JwtAuthenticatioToken实现了UsernamePasswordAuthenticationToken,主要封装了JWT token

/**
 * 自定义令牌对象
 */
public class JwtAuthenticatioToken extends UsernamePasswordAuthenticationToken {

    private static final long serialVersionUID = 1L;
    
    private String token;

    public JwtAuthenticatioToken(Object principal, Object credentials){
        super(principal, credentials);
    }
    
    public JwtAuthenticatioToken(Object principal, Object credentials, String token){
        super(principal, credentials);
        this.token = token;
    }

    public JwtAuthenticatioToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities, String token) {
        super(principal, credentials, authorities);
        this.token = token;
    }
    
    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public static long getSerialversionuid() {
        return serialVersionUID;
    }

}
View Code

 

2)接口拦截认证

1、配置分析

其实我们只需要明确一点,spring security功过容器的filter扩展机制实现的,只不过它定义了一个DelegatingFilterProxy这个filter(这个里面有引用了FiltrChainProxy,通过它可以调用security中一系列的filter),那么我们就是通过扩展或实现security中的filter来满足我们一定的需求


 

 


2、配置拦截规则

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(AuthenticationManagerBuilder auth)  {
        // 使用自定义身份验证组件
        auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService));
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 禁用 csrf, 由于使用的是JWT,我们这里不需要csrf
        http.exceptionHandling().authenticationEntryPoint(new JwtUnauthorizedEntryPoint()).and()
                .cors().and().csrf().disable()
                //.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                // 跨域预检请求
                .antMatchers("/login").permitAll()
                .antMatchers("/logout").permitAll()
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll();


        // 其他所有请求需要身份认证
        http.authorizeRequests().anyRequest().authenticated();

        // 退出登录处理器
        http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());

        // token验证过滤器
        http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
    }

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

}

JwtAuthenticationFilter

/**
 * 登录认证过滤器
 *
 */
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {

    private static final Set<String> ALLOWED_PATHS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("/login", "/register", "/actuator/health")));

    @Autowired
    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {

        String path = request.getRequestURI().substring(request.getContextPath().length()).replaceAll("[/]+$", "");
        boolean allowedPath = ALLOWED_PATHS.contains(path);
        // todo 该判断没用
        if (!allowedPath) {
            // 获取token, 并检查登录状态
            logger.debug(request.getRequestURI());
            // 构造了一个authentication,写进了spring request中
            try {
                SecurityUtils.checkAuthentication(request);
            } catch (BadCredentialsException badCredentialsException) {
                // token错误则401
                response.sendError(HttpServletResponse.SC_UNAUTHORIZED, badCredentialsException.getMessage());
                return;
            }
        }
        chain.doFilter(request, response);
    }
}
View Code

JwtUnauthorizedEntryPoint

public class JwtUnauthorizedEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED,authException.getMessage());
    }
}

三、后续
时间问题,这块后续会再完善下博客内容

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!