Security + Spring boot + jwt 多方式登录(图片验证码,手机验证码,邮箱验证码)

Security认证流程图

在这里插入图片描述

通过自定义UsernamePasswordAuthenticationFilter和BasicAuthenticationFilter 实现(验证码验证可以在UsernamePasswordAuthenticationFilter之前再加入一个Filter过滤器来处理)

使用原来的登录url (/login)

JWTLoginFilter

/**
 * 自定义JWT登录过滤器
 * 验证用户名密码正确后,生成一个token,并将token返回给客户端
 * 该类继承自UsernamePasswordAuthenticationFilter,重写了其中的3个方法
 * attemptAuthentication :接收并解析用户凭证。
 * successfulAuthentication :用户成功登录后,这个方法会被调用,我们在这个方法里生成token。
 * unsuccessfulAuthentication : 认证失败,异常往外抛,让全局异常捕捉(当然你也可以判断异常类型,返回不同的code)
 * @author cola
 */
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    public JWTLoginFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }
    
    // 尝试身份认证(接收并解析用户凭证)
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException {
        try {
            SysLoginForm loginForm = new ObjectMapper().readValue(req.getInputStream(), SysLoginForm.class);
            if(!ClassUtils.equalStringPropertyValue(LoginModeConstant.class,loginForm.getLoginMode())){
                                logger.error("未选择登录方式");
                                throw new AuthenticationServiceException("服务异常");
            }
            checkVerificationCode(loginForm);
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            loginForm.getUsername(),
                            loginForm.getPassword(),
                            new ArrayList<>())

            );
        } catch (IOException | IllegalAccessException | InstantiationException e) {
            throw new RuntimeException(e);
        }
    }

    protected void checkVerificationCode(SysLoginForm loginForm) throws GalaxyException{
        RedisUtils redisUtils = (RedisUtils) SpringContextUtils.getBean("redisUtils");
        String key = null;
        String value = null;
        if(LoginModeConstant.PHONE_VERIFICATION_CODE.equals(loginForm.getLoginMode())){
            key = RedisKeys.PHONE_VERIFICATION_LOGIN_PREFIX + loginForm.getPhone();
            value = redisUtils.get(key);
            if(StrUtil.isBlank(value) || !value.equals(loginForm.getPhoneVerificationCode())){
                throw new AuthenticationServiceException("验证码错误");
            }
        }else if(LoginModeConstant.EMAIL_VERIFICATION_CODE.equals(loginForm.getLoginMode())){
            key = RedisKeys.EMAIL_VERIFICATION_LOGIN_PREFIX + loginForm.getEmail();
            value = redisUtils.get(key);
            if(StrUtil.isBlank(value) || !value.equals(loginForm.getEmailVerificationCode())){
                throw new AuthenticationServiceException("验证码错误");
            }
        }else {
            key = RedisKeys.IMAGE_VERIFICATION_LOGIN_PREFIX + loginForm.getUuid();
            value = redisUtils.get(RedisKeys.IMAGE_VERIFICATION_LOGIN_PREFIX + loginForm.getUuid());
            if(StrUtil.isBlank(value) || !value.equals(loginForm.getImageVerificationCode())){
                throw new AuthenticationServiceException("验证码错误");
            }
        }
        redisUtils.delete(key);
    }

    // 认证成功(用户成功登录后,这个方法会被调用,我们在这个方法里生成token)
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
        // builder the token
        String token = null;
        try {
            Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
            // 定义存放角色集合的对象
            List roleList = new ArrayList<>();
            for (GrantedAuthority grantedAuthority : authorities) {
                roleList.add(grantedAuthority.getAuthority());
            }
            JwtConfig jwtConfig = (JwtConfig) SpringContextUtils.getBean("jwtConfig");
            // 生成token start
            Calendar calendar = Calendar.getInstance();
            // 签发时间
            Date now = calendar.getTime();
            // 过期时间
            Date time = new Date(now.getTime() + jwtConfig.getExpiration());
            token = Jwts.builder()
                    .setSubject(auth.getName() + "-" + roleList)
                    .setIssuedAt(now)//签发时间
                    .setExpiration(time)//过期时间
                    .signWith(SignatureAlgorithm.HS256, jwtConfig.getSecret()) //采用什么算法是可以自己选择的,不一定非要采用HS512
                    .compact();
            // 生成token end
            
            // 登录成功后,返回token
            TokenInfoDTO dto = new TokenInfoDTO();
            dto.setToken(token);
            dto.setUserName(auth.getName());

            response.setCharacterEncoding("utf-8");
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().write(JSON.toJSONString(DataResultUtil.success(dto)));
            logger.debug("认证成功");
            logger.debug("用户:" + auth.getName());
        } catch (Exception e) {
            logger.error("认证异常",e);
            response.getWriter().write(JSON.toJSONString(DataResultUtil.error("网络异常")));
        }
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException {
        logger.debug("认证失败");
        logger.debug(authenticationException.getMessage());
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json; charset=utf-8");
        response.getWriter().write(JSON.toJSONString(DataResultUtil.error(authenticationException.getMessage())));
    }
}

Security 登录异常不能全局捕捉,通过 unsuccessfulAuthentication 捕捉并返回,其实在这个地方再往外抛异常好像是可以,一开始我是这样做的,后来也失效了。就改成这样了,功力不够啊,求大神解惑。

CustomAuthenticationProvider

/**
 * 自定义身份认证验证组件
 *
 * @author
 */
public class CustomAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    private BCryptPasswordEncoder bCryptPasswordEncoder;

    public CustomAuthenticationProvider(UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder){
        this.userDetailsService = userDetailsService;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

  /**
    *执行与以下合同相同的身份验证
    * {@link org.springframework.security.authentication.AuthenticationManager#authenticate(Authentication)}
    *。
    *
    * @param authentication 身份验证请求对象。
    *
    * @返回包含凭证的经过完全认证的对象。 可能会回来
    * <code> null </ code>(如果<code> AuthenticationProvider </ code>无法支持)
    * 对传递的<code> Authentication </ code>对象的身份验证。 在这种情况下,
    * 支持所提供的下一个<code> AuthenticationProvider </ code>
    * 将尝试<code> Authentication </ code>类。
    *
    * @throws AuthenticationException 如果身份验证失败。
    */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 获取认证的用户名 & 密码
        String username = authentication.getName();
        //明文密码
        String password = authentication.getCredentials().toString();
        // 认证逻辑
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if (null != userDetails) {
            //密码比对
            if (bCryptPasswordEncoder.matches(password, userDetails.getPassword())) {
                //根据用户的名查询用户的权限
                ArrayList<GrantedAuthority> authorities = new ArrayList<>();
                Set<String> permissions = SecurityUserUtils.getUserPermissions(username);
                if(CollUtil.isNotEmpty(permissions)) {
                    for (String permission : permissions) {
                        authorities.add(new GrantedAuthorityServiceImpl(permission));
                    }
                }
                return new UsernamePasswordAuthenticationToken(username, password, authorities);
            } else {
                throw new BadCredentialsException("密码错误");
            }
        } else {
            throw new UsernameNotFoundException("用户不存在");
        }
    }

    /**
     * 是否可以提供输入类型的认证服务
     * @param authentication
     * @return
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }

}

UserDetailsServiceImpl

/**
 * @author cola
 * @version 1.0
 **/
@Service("userDetailsServiceImpl")
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    SysUserDao sysUserDao;

    //根据 账号查询用户信息
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //将来连接数据库根据账号查询用户信息
        SysUserEntity sysUserEntity = sysUserDao.queryByUserName(username);
        if(sysUserEntity == null){
            //如果用户查不到,返回null,由provider来抛出异常
            return null;
        }
        // 交给 CustomAuthenticationProvider 处理认证,这里先不查询权限标识
        return User.withUsername(sysUserEntity.getUsername()).password(sysUserEntity.getPassword()).authorities(emptyList()).build();
    }
}

LoginModeConstant

public class LoginModeConstant {
    /**
     * @author: cola
     * @date: 2020/11/30 14:21
     * @description:图片验证码
     */
    public static final String PHONE_VERIFICATION_CODE = "PHONE_VERIFICATION_CODE";

    /**
     * @author: cola
     * @date: 2020/11/30 14:21
     * @description:手机验证码
     */
    public static final String IMAGE_VERIFICATION_CODE = "IMAGE_VERIFICATION_CODE";

    /**
     * @author: cola
     * @date: 2020/11/30 14:21
     * @description:邮箱验证码
     */
    public static final String EMAIL_VERIFICATION_CODE = "EMAIL_VERIFICATION_CODE";

}

JWTAuthenticationFilter

/**
 * 自定义JWT认证过滤器
 * 该类继承自BasicAuthenticationFilter,在doFilterInternal方法中,
 * 从http头或参数 项读取token数据,然后用Jwts包提供的方法校验token的合法性。
 * 如果校验通过,就认为这是一个取得授权的合法请求
 * @author cola
 */
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {

    private static final Logger logger = LoggerFactory.getLogger(JWTAuthenticationFilter.class);

	public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String token = request.getHeader("token");
        if (StringUtil.isNullOrEmpty(token)) {
            token = request.getParameter("token");
            if(StringUtil.isNullOrEmpty(token)) {
                chain.doFilter(request, response);
                return;
            }
        }
        UsernamePasswordAuthenticationToken authentication = getAuthentication(request, response);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(request, response);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request, HttpServletResponse response) {
        long start = System.currentTimeMillis();
        String token = request.getHeader("token");
        if (StringUtil.isNullOrEmpty(token)) {
            token = request.getParameter("token");
            if(StringUtil.isNullOrEmpty(token)) {
                throw new TokenException("Token为空");
            }
        }
        // parse the token.
        String user = null;
        try {
            JwtConfig jwtConfig = (JwtConfig)SpringContextUtils.getBean("jwtConfig");
            Claims claims = Jwts.parser().setSigningKey(jwtConfig.getSecret()).parseClaimsJws(token).getBody();
            // token签发时间
			long issuedAt = claims.getIssuedAt().getTime();
			// 当前时间
			long currentTimeMillis = System.currentTimeMillis();
			// token过期时间
			long expirationTime = claims.getExpiration().getTime();
			// 1. 签发时间 < 当前时间 < (签发时间+((token过期时间-token签发时间)/2)) 不刷新token
			// 2. (签发时间+((token过期时间-token签发时间)/2)) < 当前时间 < token过期时间 刷新token并返回给前端
			// 3. tokne过期时间 < 当前时间 跳转登录,重新登录获取token
			// 验证token时间有效性
			if ((issuedAt + ((expirationTime - issuedAt) / 2)) < currentTimeMillis && currentTimeMillis < expirationTime) {
				
				// 重新生成token start
				Calendar calendar = Calendar.getInstance();
                // 签发时间
                Date now = calendar.getTime();
	            // 过期时间
	            Date time = new Date(now.getTime() + jwtConfig.getExpiration());
	            String refreshToken = Jwts.builder()
	                    .setSubject(claims.getSubject())
	                    .setIssuedAt(now)//签发时间
	                    .setExpiration(time)//过期时间
	                    .signWith(SignatureAlgorithm.HS256, jwtConfig.getSecret()) //采用什么算法是可以自己选择的,不一定非要采用HS512
	                    .compact();
	            // 重新生成token end
	            
				// 主动刷新token,并返回给前端
				response.addHeader("refreshToken", refreshToken);
			}
            long end = System.currentTimeMillis();
            logger.debug("执行时间: {}", (end - start) + " 毫秒");
            user = claims.getSubject();
            if (user != null) {
                String[] split = user.split("-")[1].split(",");
                String userName = user.split("-")[0];
                ArrayList<GrantedAuthority> authorities = new ArrayList<>();
                for (String s : split) {
                    authorities.add(new GrantedAuthorityServiceImpl(s));
                }
                return new UsernamePasswordAuthenticationToken(userName, null, authorities);
            }
        } catch (ExpiredJwtException e) {
            logger.error("Token已过期:",e);
            throw new TokenException("Token已过期");
        } catch (UnsupportedJwtException e) {
            logger.error("Token格式错误:",e);
            throw new TokenException("Token格式错误");
        } catch (MalformedJwtException e) {
            logger.error("Token没有被正确构造:",e);
            throw new TokenException("Token没有被正确构造");
        } catch (SignatureException e) {
            logger.error("签名失败:",e);
            throw new TokenException("签名失败");
        } catch (IllegalArgumentException e) {
            logger.error("非法参数异常:",e);
            throw new TokenException("非法参数异常");
        }
        return null;
    }

}

WebSecurityConfig

/**
 * SpringSecurity的配置
 * 通过SpringSecurity的配置,将JWTLoginFilter,JWTAuthenticationFilter组合在一起
 * @author zhaoxinguo on 2017/9/13.
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 需要放行的URL
     */
    @Value("#{'${auth.whitelist}'.split(',')}")
    private String[] whitelist;
    @Value("${auth.loginUrl}")
    private String loginUrl;
    @Value("${auth.logoutUrl}")
    private String logoutUrl;
    @Value("${auth.logoutSuccessUrl}")
    private String logoutSuccessUrl;

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Autowired
    private CustomAccessDeniedHandler customAccessDeniedHandler;

    @Autowired
    private CustomLogoutSuccessHandler customLogoutSuccessHandler;

    // 设置 HTTP 验证规则
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        LogoutConfigurer<HttpSecurity> httpSecurityLogoutConfigurer = http
                .formLogin()
                .loginProcessingUrl(loginUrl)
                .and()
                .cors()
                .and()
                .csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests().antMatchers(whitelist).permitAll().anyRequest().authenticated()  // 所有请求需要身份认证
                .and()
//                .exceptionHandling().authenticationEntryPoint(new Http401AuthenticationEntryPoint("Basic realm=\"MyApp\""))
//                .and()
//                .exceptionHandling().accessDeniedHandler(customAccessDeniedHandler) // 自定义访问失败处理器
//                .and()
                .addFilter(new JWTLoginFilter(authenticationManager()))
                .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                .logout() // 默认注销行为为logout,可以通过下面的方式来修改
                .logoutUrl(logoutUrl)
                .logoutSuccessUrl(logoutSuccessUrl)// 设置注销成功后跳转页面,默认是跳转到登录页面;
//                .logoutSuccessHandler(customLogoutSuccessHandler)
                .permitAll();
    }

    // 该方法是登录的时候会进入
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 使用自定义身份验证组件
        auth.authenticationProvider(new CustomAuthenticationProvider(userDetailsService, new BCryptPasswordEncoder()));
    }

}

GrantedAuthorityServiceImpl

/**
 * 权限类型,负责存储权限和角色
 *
 * @author cola
 */
public class GrantedAuthorityServiceImpl implements GrantedAuthority {

    private String authority;

    public GrantedAuthorityServiceImpl(String authority) {
        this.authority = authority;
    }

    public void setAuthority(String authority) {
        this.authority = authority;
    }

    @Override
    public String getAuthority() {
        return this.authority;
    }
}

TokenException

public class TokenException extends RuntimeException {

    private static final long serialVersionUID = 1L;

    private String msg;
    private String code = ResultEnum.UNAUTHORIZED.getValue();

    public TokenException(String msg) {
        super(msg);
        this.msg = msg;
    }

    public TokenException(String msg, Throwable e) {
        super(msg, e);
        this.msg = msg;
    }

    public TokenException(String msg, String code) {
        super(msg);
        this.msg = msg;
        this.code = code;
    }

    public TokenException(String msg, String code, Throwable e) {
        super(msg, e);
        this.msg = msg;
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }
}

配置信息

auth:
  #白名单
  whitelist: /v2/**,/swagger-ui.html,/swagger-resources/**,/doc.html,/webjars/**,/login,/captcha.jpg,/sms/login/code,/mail/login/code
  #登录url
  loginUrl: /login
  #登出url
  logoutUrl: /logout
  #登出成功跳转url
  logoutSuccessUrl: /login
  

其实实现很简单,复制代码,再修改一下报错就行(一些个性化的东西)jwt、redis集成可以自行百度。

jwt集成参考的是 springboot-springsecurity-jwt-demo

项目逻辑引用(jwt实现原理)

一: RestApi接口增加JWT认证功能

用户填入用户名密码后,与数据库里存储的用户信息进行比对,如果通过,则认证成功。传统的方法是在认证通过后,创建sesstion,并给客户端返回cookie。
现在我们采用JWT来处理用户名密码的认证。区别在于,认证通过后,服务器生成一个token,将token返回给客户端,客户端以后的所有请求都需要在http头中指定该token。
服务器接收的请求后,会对token的合法性进行验证。验证的内容包括:

内容是一个正确的JWT格式

检查签名

检查claims

检查权限

处理登录

创建一个类JWTLoginFilter,核心功能是在验证用户名密码正确后,生成一个token,并将token返回给客户端:

该类继承自UsernamePasswordAuthenticationFilter,重写了其中的2个方法:

attemptAuthentication :接收并解析用户凭证。

successfulAuthentication :用户成功登录后,这个方法会被调用,我们在这个方法里生成token。

二:授权验证

用户一旦登录成功后,会拿到token,后续的请求都会带着这个token,服务端会验证token的合法性。

创建JwtAuthenticationFilter类,我们在这个类中实现token的校验功能。

该类继承自BasicAuthenticationFilter,在doFilterInternal方法中,从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。
如果校验通过,就认为这是一个取得授权的合法请求。

三:SpringSecurity配置

通过SpringSecurity的配置,将上面的方法组合在一起。

这是标准的SpringSecurity配置内容,就不在详细说明。注意其中的

.addFilter(new JWTLoginFilter(authenticationManager()))
.addFilter(new JwtAuthenticationFilter(authenticationManager()))

这两行,将我们定义的JWT方法加入SpringSecurity的处理流程中。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值