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
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring Boot 是一个用于构建微服务的开源框架,它能够快速搭建项目并且提供了许多便捷的功能和特性。Spring Security 是一个用于处理认证和授权的框架,可以保护我们的应用程序免受恶意攻击。JWT(JSON Web Token)是一种用于身份验证的开放标准,可以被用于安全地传输信息。Spring MVC 是一个用于构建 Web 应用程序的框架,它能够处理 HTTP 请求和响应。MyBatis 是一个用于操作数据库的框架,可以简化数据库操作和提高效率。Redis 是一种高性能的键值存储系统,可以用于缓存与数据存储。 基于这些技术,可以搭建一个商城项目。Spring Boot 可以用于构建商城项目的后端服务,Spring Security 可以确保用户信息的安全性,JWT 可以用于用户的身份验证,Spring MVC 可以处理前端请求,MyBatis 可以操作数据库,Redis 可以用于缓存用户信息和商品信息。 商城项目的后端可以使用 Spring BootSpring Security 来搭建,通过 JWT 来处理用户的身份验证和授权。数据库操作可以使用 MyBatis 来简化与提高效率,同时可以利用 Redis 来缓存一些常用的数据和信息,提升系统的性能。前端请求则可以通过 Spring MVC 来处理,实现商城项目的整体功能。 综上所述,借助于 Spring BootSpring SecurityJWTSpring MVC、MyBatis 和 Redis 这些技术,可以构建出一个高性能、安全可靠的商城项目,为用户提供良好的购物体验。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值