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

一、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 基础

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值