搜索
简帛阁>技术文章>Spring Security + JWT 实现基于token的登录验证

Spring Security + JWT 实现基于token的登录验证

Spring Security + JWT 实现基于token的登录验证

    • 一、JWT
    • 二、JWT验证基本流程
    • 三、基于spring security实现登录认证
        • 前述:spring security 验证认证过程:
        • 1、基础配置
        • 2、登录认证过滤器
        • 3、登录认证生成token
        • 4、数据库中用户信息类
        • 5、自定义令牌
        • 6、自定义 provider
        • 7、权限集合的封装类
        • 8、DaoAuthenticationProvider 源码
    • 四、总结:
    • 五、参考链接:

一、JWT

JWT:json web token,是目前最流行的一个跨域认证解决方案:客户端发起用户登录请求,服务器端接收并认证成功后,生成一个 JSON 对象,然后将其返回给客户端。

二、JWT验证基本流程

1.用户携带username和password请登录
2.服务器验证登录验证,如果验证成功,根据用户的信息和服务器的规则生成JWT Token
3.服务器将该token返回
4.用户得到token,存在localStorage、cookie或其它数据存储形式中。
5.以后用户请求服务器时,在请求的header中加入 Authorization:xxxx(token) 。服务器端对此token进行检验,如果合法就解析其中内容,根据其拥有的权限和业务逻辑反回响应结果。

三、基于spring security实现登录认证

前述:spring security 验证认证过程:

1.用户使用username和password登录;
2 用户名和密码被过滤器获取到,封装成Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
3 AuthenticationManager 身份管理器负责验证这个Authentication
4 认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例。
5 SecurityContextHolder安全上下文容器将第4步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。

1、基础配置

/**
 * @Description 安全配置
 * @Date 
 *
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyUserDetailService myUserDetailService;

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    /**
     * 实例化JwtAuthenticationProvider
     *
     * @return
     */
    @Bean
    JwtAuthenticationProvider authenticationProvider() {
        JwtAuthenticationProvider jwtAuthenticationProvider = new JwtAuthenticationProvider(myUserDetailService);
        jwtAuthenticationProvider.setPasswordEncoder(passwordEncoder);
        return jwtAuthenticationProvider;
    }

    /**
     * 将provider添加到authenticationProviders集合中
     * 在ProviderManager.authenticate(Authentication authentication)方法中会调用相关的provider
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider());
    }

    /**
     * 配置spring secrurity
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 禁用 csrf, 由于使用的是JWT,我们这里不需要csrf
        http.cors().and().csrf().disable()
                .authorizeRequests()
                // 跨域预检请求
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                // 查看SQL监控(druid)
                .antMatchers("/druid/**").permitAll()
                // 首页和登录页面
                .antMatchers("/").permitAll()
                .antMatchers("/unAuth/**").permitAll()
                .antMatchers("/accessToken/**").permitAll()
                .antMatchers("/emp/**").permitAll()
                .antMatchers("/employee/**").permitAll()
                .antMatchers("/customer/**").permitAll()
                // 服务监控
                .antMatchers("/actuator/**").permitAll()
                // swagger
                .antMatchers("/swagger-ui.html").permitAll()
                .antMatchers("/swagger-resources/**").permitAll()
                .antMatchers("/configuration/**").permitAll()
                .antMatchers("/v2/api-docs").permitAll()
                .antMatchers("/webjars/**").permitAll()
                //供内部RPC
                .antMatchers("/verificationCodeEx/**").permitAll()
                .antMatchers("/authClientEx/**").permitAll()
                //IAM相关
                .antMatchers("/iam/**").permitAll()
                // 其他所有请求需要身份认证
                .anyRequest().authenticated();
        // 退出登录处理器
        http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());
        // token验证过滤器
        http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
    }

    /**
     * 获取AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

2、登录认证过滤器

/**
 * 登录认证过滤器
 * 继承 BasicAuthenticationFilter,在访问任何URL的时候会被此过滤器拦截
 */
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {

    /**
     * 构造器
     *
     * @param authenticationManager
     */
    @Autowired
    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    /**
     * 过滤逻辑
     *
     * @param request
     * @param response
     * @param chain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 从http请求中获取token, 并在上下文中记录认证信息
//        SecurityUtils.checkAuthentication(request);
        chain.doFilter(request, response);
    }

}

3、登录认证生成token

/**
     * PC端,员工登录,手机号+短信验证码
     * 无需认证,直接对外开放
     */
    @ApiOperation(value = "PC端员工通过 手机号+短信验证码 登录")
    @PostMapping(value = "/unAuth/loginPC2")
    public HttpResult loginPCByMobileSMS(@RequestBody LoginBean loginBean, HttpServletRequest request) throws Exception {
        JwtAuthenticatioToken token = this.employeeLoginMainService.loginByMobileSMS(loginBean, request);
        //擦除密码
        token.eraseCredentials();
        return HttpResult.successWithData(token);
    }
 /**
     * 员工通过 手机号+短信验证码 登录并生成token
     *
     * @param loginBean
     * @param request
     * @return
     * @throws Exception
     */
    public JwtAuthenticatioToken loginByMobileSMS(LoginBean loginBean, HttpServletRequest request) throws Exception {
        JwtAuthenticatioToken token = this.employeeLoginBaseService.handleLogin(loginBean, request);
        
        return token;
    }
/**
     * 生成token的核心逻辑
     * @param loginBean
     * @param request
     * @return
     */
    @Override
    public JwtAuthenticatioToken handleLogin(LoginBean loginBean, HttpServletRequest request) {
        // 系统登录认证,生产token
        Map map = SecurityUtils.login(request, loginBean, this.authenticationManager);
       
        String token = AuthConstants.TOKEN_PREFIX + RandomStringUtils.randomAlphanumeric(10) + String.valueOf(idGenerator.nextId());
        String authorities = (String) map.get("authorities");
        Map<String, Object> claims = (Map<String, Object>) map.get("claims");

        //保存新token
        this.redisService.saveToken(token,
                JSON.toJSONString(new TokenValueInRedis(authorities, claims)),
                LoginRelatedHelper.getLoginDeviceType(loginBean.getUserLoginType()));

      
        //员工登陆表中记录客户登陆成功后产生的token

        
        return new JwtAuthenticatioToken(loginBean.getName(), null, token);
    }
/**
 * Security相关操作
 */
public class SecurityUtils {

    /**
     * 构造用于生产令牌的Claims
     */
    public static Map<String, Object> buildClaimsMap(LoginBean loginBean) {
        Map<java.lang.String, java.lang.Object> claims = new HashMap<>(10);
        claims.put(JwtTokenUtils.USERID, loginBean.getId());
        claims.put(JwtTokenUtils.USERNAME, loginBean.getName());
        claims.put(JwtTokenUtils.USERLOGINTYPE, loginBean.getUserLoginType());
        claims.put(JwtTokenUtils.COMPANYID, loginBean.getCompanyId());
        claims.put(JwtTokenUtils.COMPANYNAME, loginBean.getCompanyName());
        claims.put(JwtTokenUtils.DEPTID, loginBean.getDeptId());
        claims.put(JwtTokenUtils.DEPTNAME, loginBean.getDeptName());
        claims.put(JwtTokenUtils.CREATED, new Date());
        claims.put(JwtTokenUtils.AUTHORITIES, "");
        claims.put(JwtTokenUtils.CLIENTID, loginBean.getClientId() == null ? "" : loginBean.getClientId());
        claims.put(JwtTokenUtils.INVITATIONCODE, loginBean.getInvitationCode()); //邀请码
        return claims;
    }

    /**
     * 生成令牌相关的 Claims
     */
    public static Map<String, Object> generateClaims(Authentication authentication, LoginBean loginBean) {
        Long userid = getUserID(authentication);
        String username = getUsername(authentication);

        loginBean.setId(userid);
        loginBean.setName(username);


        Map<String, Object> claims = buildClaimsMap(loginBean);

//        return JwtTokenUtils.generateToken(claims);
        return claims;
    }


    /**
     * 系统登录认证
     *
     */
    public static Map<String, Object> login(HttpServletRequest request, LoginBean loginBean, AuthenticationManager authenticationManager) {
        JwtAuthenticatioToken token = new JwtAuthenticatioToken(loginBean.getAccount(), loginBean.getPassword());
        token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        // 执行登录认证过程
        Authentication authentication = authenticationManager.authenticate(token);
        // 认证成功存储认证信息到上下文
        SecurityContextHolder.getContext().setAuthentication(authentication);
       
        Map<String, Object> claims = generateClaims(authentication, loginBean);

        HashMap<String, Object> map = new HashMap<>();
        map.put("claims", claims);
        String authorites = authentication.getAuthorities().stream().map(authority -> ((GrantedAuthority) authority).getAuthority()).collect(Collectors.joining(";"));
        map.put("authorities", authorites);

        return map;
    }

    /**
     * 获取用户id
     * @return
     */
    private static Long getUserID(Authentication authentication) {
        Long userid = null;
        if (authentication != null) {
            Object principal = authentication.getPrincipal();
            if (principal instanceof JwtUserDetails) {
                userid = ((JwtUserDetails) principal).getUserid();
            }
        }
        return userid;
    }

    /**
     * 获取用户名
     * @return
     */
    private static String getUsername(Authentication authentication) {
        String username = null;
        if (authentication != null) {
            Object principal = authentication.getPrincipal();
            if (principal instanceof JwtUserDetails) {
                username = ((JwtUserDetails) principal).getUsername();
            }
        }
        return username;
    }
}

4、数据库中用户信息类

/**
 * 自定义用户模型,用于JWT
 */
public class JwtUserDetails implements UserDetails {
    private Long userid;
    private String username;
    private String password;
    private String salt;
    private Collection<? extends GrantedAuthority> authorities;

    /**
     * 构造器
     *
     * @param userid
     * @param username
     * @param password
     * @param salt
     * @param authorities
     */
    public JwtUserDetails(Long userid, String username, String password, String salt, Collection<? extends GrantedAuthority> authorities) {
        this.userid = userid;
        this.username = username;
        this.password = password;
        this.salt = salt;
        this.authorities = authorities;
    }

    /**
     * 构造器
     *
     * @param userid
     * @param username
     * @param password
     * @param authorities
     */
    public JwtUserDetails(Long userid, String username, String password, Collection<? extends GrantedAuthority> authorities) {
        this(userid, username, password, DEFAULT_SALT, authorities);
    }

    /**
     * 获取用户id
     *
     * @return
     */
    public Long getUserid() {
        return userid;
    }

    /**
     * 获取用户姓名
     *
     * @return
     */
    @Override
    public String getUsername() {
        return username;
    }

    /**
     * 获取密码
     *
     * @return
     */
    @JsonIgnore
    @Override
    public String getPassword() {
        return password;
    }

    /**
     * 获取盐
     *
     * @return
     */
    public String getSalt() {
        return salt;
    }

    /**
     * 获取权限标识集合
     *
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    /**
     * 账户是否过期
     *
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 账户是否锁定
     *
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 认证是否过期
     *
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 是否有效
     *
     * @return
     */
    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return true;
    }
}

5、自定义令牌

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

    private static final long serialVersionUID = 1L;

    private String token;

    /**
     * 构造器
     *
     * @param principal
     * @param credentials
     */
    public JwtAuthenticatioToken(Object principal, Object credentials) {
        super(principal, credentials);
    }

    /**
     * 构造器
     *
     * @param principal
     * @param credentials
     * @param token
     */
    public JwtAuthenticatioToken(Object principal, Object credentials, String token) {
        super(principal, credentials);
        this.token = token;
    }

    /**
     * 构造器
     *
     * @param principal
     * @param credentials
     * @param authorities
     * @param token
     */
    public JwtAuthenticatioToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities, String token) {
        super(principal, credentials, authorities);
        this.token = token;
    }

    /**
     * 获取token
     *
     * @return
     */
    public String getToken() {
        return token;
    }

    /**
     * 设置token
     *
     * @param token
     */
    public void setToken(String token) {
        this.token = token;
    }

    /**
     * 获取序列化id
     *
     * @return
     */
    public static long getSerialversionuid() {
        return serialVersionUID;
    }

}

6、自定义 provider

/**
 * 自定义 provider
 * additionalAuthenticationChecks 方法中进行密码正确性校验
 */
@Data
@Slf4j
public class JwtAuthenticationProvider extends DaoAuthenticationProvider {
    //是否跳过基于SpringSecurity的密码校验
    public static ThreadLocal<Boolean> isNeedCheckPassword = ThreadLocal.withInitial(() -> true);

    private BCryptPasswordEncoder passwordEncoder;

    /**
     * 构造器
     *
     * @param userDetailService
     */
    public JwtAuthenticationProvider(UserDetailsService userDetailService) {
        setUserDetailsService(userDetailService);
    }

    /**
     * 自定义的密码校验
     *
     * @param userDetails
     * @param authentication
     * @throws AuthenticationException
     */
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        //先判断是否需要用户名和密码校验
        if (!isNeedCheckPassword.get()) {
            return;
        }

        if (authentication.getCredentials() == null) {
            throw new BaseException(HttpStatus.BadRequest.getStatus(), "请输入密码!");
        }

        //用户录入的密码,明文
        String presentedPassword = authentication.getCredentials().toString();
        //盐值
        String salt = ((JwtUserDetails) userDetails).getSalt();
        // 覆写密码验证逻辑
        if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            throw new BaseException(HttpStatus.BadRequest.getStatus(), "用户名密码错误");
        }
    }

   
}

7、权限集合的封装类

/**
 * 权限集合的封装类
 */
public class GrantedAuthorityImpl implements GrantedAuthority {

    private static final long serialVersionUID = 1L;

    private String authority;

    /**
     * 构造器
     *
     * @param authority
     */
    public GrantedAuthorityImpl(String authority) {
        this.authority = authority;
    }

    /**
     * 设置权限
     *
     * @param authority
     */
    public void setAuthority(String authority) {
        this.authority = authority;
    }

    /**
     * 获取权限
     *
     * @return
     */
    @Override
    public String getAuthority() {
        return this.authority;
    }
}

8、DaoAuthenticationProvider 源码

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.security.authentication.dao;

import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.Assert;

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
    private PasswordEncoder passwordEncoder;
    private volatile String userNotFoundEncodedPassword;
    private UserDetailsService userDetailsService;
    private UserDetailsPasswordService userDetailsPasswordService;

    public DaoAuthenticationProvider() {
        this.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
    }

    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();
            if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                this.logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }

    protected void doAfterPropertiesSet() {
        Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
    }

    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        this.prepareTimingAttackProtection();

        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
            } else {
                return loadedUser;
            }
        } catch (UsernameNotFoundException var4) {
            this.mitigateAgainstTimingAttack(authentication);
            throw var4;
        } catch (InternalAuthenticationServiceException var5) {
            throw var5;
        } catch (Exception var6) {
            throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
        }
    }

    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
        boolean upgradeEncoding = this.userDetailsPasswordService != null && this.passwordEncoder.upgradeEncoding(user.getPassword());
        if (upgradeEncoding) {
            String presentedPassword = authentication.getCredentials().toString();
            String newPassword = this.passwordEncoder.encode(presentedPassword);
            user = this.userDetailsPasswordService.updatePassword(user, newPassword);
        }

        return super.createSuccessAuthentication(principal, authentication, user);
    }

    private void prepareTimingAttackProtection() {
        if (this.userNotFoundEncodedPassword == null) {
            this.userNotFoundEncodedPassword = this.passwordEncoder.encode("userNotFoundPassword");
        }

    }

    private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
        if (authentication.getCredentials() != null) {
            String presentedPassword = authentication.getCredentials().toString();
            this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
        }

    }

    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
        this.passwordEncoder = passwordEncoder;
        this.userNotFoundEncodedPassword = null;
    }

    protected PasswordEncoder getPasswordEncoder() {
        return this.passwordEncoder;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    protected UserDetailsService getUserDetailsService() {
        return this.userDetailsService;
    }

    public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService) {
        this.userDetailsPasswordService = userDetailsPasswordService;
    }
}

四、总结:

spring security验证过程:
1.userName、passWord生成 JwtAuthenticatioToken(JwtAuthenticatioToken token = new JwtAuthenticatioToken(loginBean.getAccount(), loginBean.getPassword());)
2.AuthenticationManager进行验证(Authentication authentication = authenticationManager.authenticate(token);)
配置文件中myUserDetailService实例化JwtAuthenticationProvider(继承DaoAuthenticationProvider–>AuthenticationManager的实现类)
DaoAuthenticationProvider 调用 本身retrieveUser方法,此方法再调用myUserDetailService 的loadUserByUsername方法查询用户信息、用户权限,DaoAuthenticationProvider 调用本身additionalAuthenticationChecks方法进行密码校验,验证通过返回填充满的Authentication 对象,不通过报错;
3.将Authentication 对象设置进SecurityContextHolder中(SecurityContextHolder.getContext().setAuthentication(authentication);)
SecurityContextHolder默认使用ThreadLocal 策略来存储认证信息。看到ThreadLocal 也就意味着,这是一种与线程绑定的策略。

五、参考链接:

spring security 基础

SSO:同一个帐号在同一个公司不同系统上登陆使用SpringSecurity实现类似于SSO登陆系统是十分简单下面我就搭建一个DEMO首先来看看目录结构其中ssodemo是父工程项目ssoclie
文章目录一、JWT是什么二、使用步骤1项目结构2相关依赖3数据库4相关代码三、测试结果一、JWT是什么在介绍JWT之前我们先来回顾一下利用token进行用户身份验证流程1、客户端使用用户名和密码请
最近在做项目过程中需要用JWT登录和鉴权查了很多资料都不甚详细有的是需要在applicationyml里进行jwt配置但我在导包后并没有相应配置项因而并不适用在踩过很多坑之后稍微整理了一下做个
最近有个粉丝提了个问题,说他在SpringSecurity中用JWT做退出登录时无法获取当前用户,导致无法证明“我就是要退出那个我”,业务失败!经过我一番排查找到了原因,而且这个错误包括我自己
目源码:https://giteecom/tanwubo/jwtspringsecuritydemo登录通过自定义WxAppletAuthenticationFilter替换默认Username
序SpringSecurityOAuth2demo在前几篇文章中已经讲过了,在那些模式中使用都是RemoteTokenService调用授权服务器来校验token,返回校验通过用户信息供上下文中
目录序授权服务器整合JWT——对称加解密算法资源服务器整合JWT——对称加解密算法OAuth整合JWT——非对称加解密RSA测试验证测试通过序SpringSecurityOAuth2demo在前几篇
录一、登录校验流程1、SpringSecurity完整流程2、SpringSecurity默认登陆验证流程。3、整合JWT大致流程前端响应类JWT工具类重写UserDetailsService
1简介SpringSecurity作为成熟且强大安全框架,得到许多大厂青睐。而作为前后端分离SSO方案,JWT也在许多项目中应用。本文将介绍如何通过SpringSecurity实现JWT认证。用
jwt总结与实现请求和响应请求实体规定客户端传给jwt认证服务器参数响应实体规定了jwt服务端颁发给客户端jwttoken结果jwtUtil类主要提供了jwt实现方法,如加密规则,生成t