欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

如何用Spring Security和JWT轻松实现用户登录验证与权限控制

最编程 2024-07-28 11:04:07
...

引言

在Web应用开发中,安全一直是非常重要的一个方面。Spring Security基于Spring 框架,提供了一套Web应用安全性的完整解决方案。


JwT (JSON Web Token) 是当前比较主源的Token令牌生成方案,非常适合作为登录和授权认证的凭证。


这里我们就使用 Spring Security并结合JWT实现用户认证(Authentication) 和用户授权(Authorization) 两个主要部分的安全内容。

一、JWT与OAuth2的区别

在此之前,只是停留在用的阶段,对二者的使用场景很是模糊,感觉都是一样的呀,有啥不同呢,这里我也是根据网上的指点,在这罗列一下。


1、跨域实现不同

首先是涉及到跨域的问题:

如果ABC三个系统是相同域名的,比如都是www.a.com,那么就可以使用JWT的方式,将三个系统改造成统一的一个登录和拦截校验。


如果ABC不是相同域名的,比如:

www. a.com,www.b.com,www.c.com,建议不要使用JWT这种方式,因为需要涉及到跨域,这样跨域获取token可能存在安全问题,可以考虑使用传统cookie+session方式来实现跨域的SSO机制。或者可以使用SpringBoot+Security+OAuth2来实现,这就涉及到了OAuth2了.


2、所属性质原理不同

OAuth2是一种授权框架


OAuth2是一种授权框架,提供了一套详细的授权机制(指导)。用户或应用可以通过公开的或私有的设置,授权第三方应用访问特定资源。


JWT是一种认证协议


JWT提供了一种用于发布接入令牌(Access Token),并对发布的签名接入令牌进行验证的方法。 令牌(Token)本身包含了一系列声明,应用程序可以根据这些声明限制用户对资源的访问。


3、应用场景不同

OAuth2用在使用第三方账号登录的情况(比如使用weibo, qq, github登录某个app)


JWT是用在前后端分离, 需要简单的对后台API进行保护时使用.(前后端分离无session, 频繁传用户密码不安全)


OAuth2是一个相对复杂的协议, 有4种授权模式, 其中的access code模式在实现时可以使用jwt才生成code, 也可以不用. 它们之间没有必然的联系;

OAuth2有client和scope的概念,jwt没有。

如果只是拿来用于颁布token的话,二者没区别,常用的bearer算法oauth、jwt都可以用,只是应用场景不同而已。


具体的比较推荐给大家一片文章,写的很详细

https://blog.****.net/A15712399740/article/details/95233903

二、正题Spring Security并结合JWT实现用户认证和用户授权

2.1、添加pom.xml依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
</dependency>

2.2、添加配置文件WebSecurityConfig

在config 包下新建一个Spring Security 的配置类WebSecurityConfig, 主要是进行一些安全相关的配置,比如权限URL匹配策略、认证过滤器配置、定制身份验证组件、开启权限认证注解等,具体代码作用参见代码注释。

/**
 * @program: mangocms
 * @description: 安全配置类
 * @author: zjc
 * @create: 2020-08-05 19:53
 **/
@Configuration
    @EnableWebSecurity //开启Spring Security
    @EnableGlobalMethodSecurity(prePostEnabled = true) //开启权限注解 例如:@PreAuthorize注解
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private UserDetailsService userDetailsService;

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //禁用csrf,由于使用的是jwt,我们这里不需要csrf
        http.cors().and().csrf().disable().authorizeRequests()
                //跨域预检请求
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                //web jars
                .antMatchers("/webjars/**").permitAll()
                //查看SQL监控(druid)
                .antMatchers("/druid/**").permitAll()
                //首页和登录页面
                .antMatchers("/").permitAll().antMatchers("/login").permitAll()
                //swagger
                .antMatchers("/swagger-ui.html").permitAll()
                .antMatchers("/swagger-resources/**").permitAll()
                .antMatchers("/v2/api-docs").permitAll()
                .antMatchers("/webjars/springfox-swagger-ui/**").permitAll()
                //验证码
                .antMatchers("/captcha.jpg**").permitAll()
                //服务监控
                .antMatchers("/actuator/**").permitAll()
                //其他所有请求需要身份验证
                .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();
    }
}

2.3、JwtAurthenticationFilter登录认证过滤器

登录认证过滤器负责登录认证时检查并生产令牌并保存到上下文,接口权限认证过程时,系统从上下文获取令牌校验接口访问权限,新建一个security包,在其下创建JwtAurthenticationFilter并继承BasicAuthenticationFilter, 覆写其中的doFilterlntermal 方法进行Token校验。

/**
 * @program: mangocms
 * @description: 登录认证过滤器
 * @author: zjc
 * @create: 2020-08-05 20:53
 **/
@Configuration
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
    @Autowired
    public JwtAuthenticationFilter(AuthenticationManager authenticationManager){
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //获取token,并检查登陆状态,检查request中的请求信息
        SecurityUtils.checkAuthentication(request);
        chain.doFilter(request,response);
    }
}

这里我们把验证逻辑抽取到了SecurityUtils 的checkAuthentication 方法中,checkAuthentication通过JwtTokenUtils的方法获取认证信息并保存到Spring Security上下文中。


2.4、SecurityUtils获取令牌并进行认证

 /**
     * 获取令牌进行认证
     */
    public  static void checkAuthentication(HttpServletRequest request){
        //获取令牌并根据令牌获取登录认证信息
        Authentication authentication =JwtTokenUtils.getAuthentticattionFromToken(request);
        //设置登录认证信息到上下文
        SecurityContextHolder.getContext().setAuthentication(authentication);
    }

2.5、JwtTokenUtils根据请求令牌获取登录认证信息

这里只是再进行主要方法的逻辑追踪,后面我会附上完整的代码下载地址。

/**
     * 根据请求令牌获取登录认证信息
     *
     */
    public static Authentication getAuthentticattionFromToken(HttpServletRequest request){
        Authentication authentication =null;
        //获取请求携带的令牌
        String token = JwtTokenUtils.getToken(request);
        if (token != null){
            //请求令牌并不能为空
            if(SecurityUtils.getAuthentication() == null){
                //上下文中的Authentication
                Claims claims = getClaimsFromToken(token);
                if(claims == null){
                    return null;
                }
                String username =claims.getSubject();
                if(username == null){
                    return null;
                }
                if(isTokenExpired(token)){
                    return null;
                }
                Object authors =claims.get(AUTHORITIES);
                List<GrantedAuthority> authorities =new ArrayList<>();
                if(authors != null && authors instanceof List){
                    for(Object object : (List) authors){
                        authorities.add(new GrantedAuthorityImpl(
                                (String)((Map) object).get("authority")));
                    }
                }
                authentication = new JwtAuthenticatioToken(username,null,authorities,token);
            }else{
                if(validateToken(token,SecurityUtils.getUsername())){
                    //如果上下文中Authentication非空,且请求命令合法
                    //直接返回当前登录认证信息
                    authentication = SecurityUtils.getAuthentication();
                }
            }
        }
        return authentication;
    }
    
    /**
     * 获取请求Token
     * @param request
     * @return
     */
    //尝试从请求头中获取请求写带的令牌,默认从请求头中的“Authentication”参数以“Bearer”开头的信息为令牌信息,
    //若为空的话,尝试从token参数获取
    public static String getToken(HttpServletRequest request){
        String token = request.getHeader("Authorization");
        String tokenHead = "Bearer";
        if(token == null){
            token = request.getHeader("token");
        }else if(token.contains(tokenHead)){
            //当且仅当此字符串包含指定的tokenHead值序列时,返回true。
            token =token.substring(tokenHead.length());
        }
        if("".equals(token)){
            token = null;
        }
        return token;
    }

2.6身份认证组件

SpringSecurity 的登录验证是交由ProviderManager负责的,ProviderManager 在实际验证时其通过调用AuthenticationProvider的authenticate方法来进行认证。数据库类型的默认实现方案是DaoAuthenticationProvider。我们这里通过继承DaoAuthenticationProvider 定制默认的登录认证逻辑,在Security 包下新建验证器JwtAuthenticationProvider并继承DaoAuthenicationProvider,覆盖实现additionalAuthenticationChecks方法进行密码匹配,我们这里没有使用默认的密码认证器 (我们使用盐salt来对密码加密,默认密码验证器没有加盐),所以这里定制了自己的密码校验逻辑, 当然你也可以通过直接覆写authenticate方法来完成更大范围的登录认证需求定制。


JwtAuthenticationProvider身份验证提供者

/**
 * @program: mangocms
 * @description: 身份认证提供者
 * @author: zjc
 * @create: 2020-08-08 09:35
 **/

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("Authentraction failed: no credentials provided");
            throw new BadCredentialsException(messages.getMessage("AbstractUserDetalisAuthenticationProvider.badCredentials","Bad credentials"));
        }
        String presentedPassword = authentication.getCredentials().toString();
        String salt = ((JwtUserDetails)userDetails).getSalt();
        if(! new PasswordEncoder(salt).matches(userDetails.getPassword(),presentedPassword)){
            //覆写密码验证逻辑  matches:匹配两者
            logger.debug("Authentication failed: password does not match");
            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"
            ));
        }
    }
}

推荐阅读