微服务-权限认证-Security-jwt实战

学习思路

  1. 认证流程
  2. 基于Spring-Security原理讲解
  3. 用户名密码模式具体代码实现
  4. 根据用户名密码模式思考短信验证码模式

一、认证流程

用户登录、认证流程

  1. 用户输入用户名密码请求后台服务
  2. 后台验证用户名密码是否正确
  3. 正确返回token,错误抛出异常
  4. 用户拿到token请求非白名单服务
  5. 如果token正常访问成功,如果token异常访问失败

题外话:有没有刚刚入门写demo的感觉

二、基于Spring-Security原理讲解

首先看一下用户名密码认证的源码时序图

 解释:

  1. AuthencationManager对usernamePasswordAuthencationToken进行认证
  2. Manager又将token交给了与该token相匹配的AuthenticationProvider(可多个provider)
  3. Provider调用本地UserDetailService获取用户信息
  4. provider进行密码校验
  5. 校验通过发送成功事件,进入成功后处理,失败抛出异常

三、用户名密码模式具体代码实现

由于Security默认实现了用户名密码模式

1、引入jar包

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

2、编写Filter集成AbstractAuthenticationProcessingFilter,实现attemptAuthentication方法返回Authentication即可

public class UsernamePasswordFilter extends AbstractAuthenticationProcessingFilter {

	@Autowired
	LoginExtendProperty loginExtendProperty;
	@Autowired
	UserExtendService userExtendService;


	/**
	 * 重新定义登陆地址
	 * @param authenticationManager
	 */
    public UsernamePasswordFilter(AuthenticationManager authenticationManager, String loginUrl) {
        super(new AntPathRequestMatcher(loginUrl, HttpMethod.POST.name()));
        setAuthenticationManager(authenticationManager);
		setRequiresAuthenticationRequestMatcher(new RequestMatcher(MatcherRule.USERNAME_PASSWORD.getRuleName()));
    }

	/**
	 * 从body取用户名密码
	 * @return
	 * @throws AuthenticationServiceException
	 * @throws IOException
	 */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationServiceException, IOException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        if (!userExtendService.validate(request)){
			throw new AuthenticationServiceException("业务验证异常");
		}
		String json = StreamUtils.copyToString(request.getInputStream(), Charset.forName("UTF-8"));
        logger.info(json);
		JSONObject jsonObject;
        if (null !=json && !"".equals(json) && json.contains(loginExtendProperty.getUsername()) && json.contains(loginExtendProperty.getPassword())){
        	jsonObject=JSONObject.parseObject(json);
		}else {
        	throw new AuthenticationServiceException("用户名或密码错误");
		}
		return this.getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(
                jsonObject.get(loginExtendProperty.getUsername()), jsonObject.get(loginExtendProperty.getPassword())
        ));
    }
}

其中UsernamePasswordAuthenticationToken内部已经提供实现,我们只需要将用户名密码从request里拿出来new对象即可

3、将配置Filter,编写config继承WebSecurityConfigurerAdapter


@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@ComponentScan(basePackages = {"com.murphy.security"})
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthFilterProperty jwtAuthFilterProperty;

    @Autowired
    LoginExtendProperty loginExtendProperty;

    @Autowired
    private SimpleAuthenticatingSuccessHandler simpleAuthenticatingSuccessHandler;

    @Autowired
    private SimpleAuthenticatingFailureHandler simpleAuthenticatingFailureHandler;

    @Autowired
    UserExtendService userExtendService;

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * HTTP请求安全处理
     * token请求授权
     *
     * @param httpSecurity .
     * @throws Exception .
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {


        // 由于使用的是JWT,我们这里不需要csrf
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry expressionInterceptUrlRegistry
                = httpSecurity.csrf().disable()
                //未授权处理
                .exceptionHandling().authenticationEntryPoint((request, response, authException) -> {
                    logger.error("未授权uri={}",request.getRequestURI());
                    response.setHeader("Access-Control-Allow-Origin", "*");
                    response.setStatus(200);
                    Map<String,Object> map=new HashMap<>(3);
                    map.put("code","401");
                    map.put("message","未授权uri="+request.getRequestURI());
                    map.put("data",Boolean.FALSE);
                    response.setCharacterEncoding("UTF-8");
                    JSONObject.writeJSONString(response.getWriter(),map);
                })
                // 基于token,所以不需要session
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().authorizeRequests();
        //白名单,无需token
        String[] urls = jwtAuthFilterProperty.getExceptUrl().split(",");
        for (String url : urls) {
            // 对于获取token的rest api要允许匿名访问
            expressionInterceptUrlRegistry.antMatchers(url).permitAll();
        }
        expressionInterceptUrlRegistry
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        // 添加JWT filter
        //将token验证添加在密码验证前面
        httpSecurity.addFilterBefore(getJwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        httpSecurity.addFilterBefore(getUsernamePasswordFilter(), JwtAuthenticationTokenFilter.class);
        httpSecurity.authenticationProvider(new SmsAuthenticationProvider(userExtendService)).addFilterBefore(getSmsFilter(),UsernamePasswordFilter.class);
        // 禁用缓存
        httpSecurity.headers().cacheControl();
    }
	/**
	 * 	密码加密
	 * @author dongsufeng
	 * @return org.springframework.security.crypto.password.PasswordEncoder
	 * @date 2019/9/17 18:39
	 */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    public JwtAuthenticationTokenFilter getJwtAuthenticationTokenFilter() throws Exception {
        return new JwtAuthenticationTokenFilter();
    }

    @Bean
    public AuthenticationManager getManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    @Bean
    public UsernamePasswordFilter getUsernamePasswordFilter() throws Exception {
        UsernamePasswordFilter userLoginFilter = new UsernamePasswordFilter(getManagerBean(), loginExtendProperty.getLoginUrl());
        userLoginFilter.setAuthenticationSuccessHandler(simpleAuthenticatingSuccessHandler);
        userLoginFilter.setAuthenticationFailureHandler(simpleAuthenticatingFailureHandler);
        return userLoginFilter;
    }


}

解释:

  1. configure()方法是http安全请求处理,主要配置了一些白名单,那些拦截,Filter及顺序
  2. getManagerBean()是获取AuthenticationManager
  3. getUsernamePasswordFilter()里边配置了执行成功和失败的Handler

4、编写执行成功,失败的Handler

/**
 * 登陆成功后处理
 * @author dongsufeng
 * @date 2019/12/02 15:01
 * @version 1.0
 */
@Component
public class SimpleAuthenticatingSuccessHandler implements AuthenticationSuccessHandler {

    private static ObjectMapper globalObjectMapper = new ObjectMapper();

    @Autowired
    private JwtService jwtService;

    static {
        globalObjectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    }

    private Logger logger = LoggerFactory.getLogger(SimpleAuthenticatingSuccessHandler.class);

    private static void clearAuthenticationAttributes(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return;
        }
        session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
    }

	/**
	 * 登陆成功返回token给用户
	 * @param authentication
	 * @throws IOException
	 * @throws ServletException
	 */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        Map<String, Object> result = new HashMap<>();
        logger.info("登录成功返回对象==={}",JSONObject.toJSONString(authentication.getPrincipal()));
        result.put("token", jwtService.generateToken(authentication));
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        globalObjectMapper.writeValue(response.getWriter(), result);
        clearAuthenticationAttributes(request);
    }

}

解释:

  1. 主要实现AuthenticationSuccessHandler接口实现onAuthenticationSuccess方法
  2. 成功后生成token:jwtService.generateToken(authentication)
  3. 封装token返回
  4. 异常情况实现AuthenticationFailureHandler接口跟成功差不多

5、生成token,看注释,可以适当将用户信息放入

/**
     * 生成令牌
     *
     * @param  .
     * @return .
     */
    public String generateToken(Authentication authentication) {
        return Jwts.builder()
                //jwt签发者
                .setIssuer(jwtPayloadProperties.getIssuer())
                // jwt所面向的用户
                .setSubject(authentication.getName())
                //接收jwt的一方
                .setAudience(jwtPayloadProperties.getAudience())
                //生效时间
                .setNotBefore(new Date(System.currentTimeMillis() - jwtPayloadProperties.getNotBeforeMinute() * 60 * 1000))
                //失效时间
                .setExpiration(new Date(System.currentTimeMillis() + jwtPayloadProperties.getExpirationMinute() * 60 * 1000))
                //签发时间
                .setIssuedAt(new Date())
                //秘钥签名
                .signWith(SignatureAlgorithm.HS512, jwtPayloadProperties.getSecret())
                .claim("userDTO", JSON.toJSONString(authentication.getPrincipal()))
                .compact();
    }

6、作为使用者应该实现UserDetailsService接口实现loadUserByUsername()

@Component
@Log4j2
public class UserDetailsService implements UserExtendService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if ("18888888888".equals(username)) {
            User user = new User("18888888888", PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123456"), "userid1111101111");
            user.setMobile("13333333333");
            return user;
        }else {
            return null;
        }
    }
}

这里主要通过username查询用户信息,这个必须实现,主要用于providr里获取用户信息

7、用户拿到token后可以将token放入header然后请求相应的服务了,进入验证环境

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JwtService jwtService;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtAuthFilterProperty jwtAuthFilterProperty;

    /**
     * 过滤器逻辑
     *
     * @param request  .
     * @param response .
     * @param chain    .
     * @throws ServletException .
     * @throws IOException      ,
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String authHeader = request.getHeader(this.jwtAuthFilterProperty.getHeader());
        String tokenHead = this.jwtAuthFilterProperty.getTokenHead();
        if (authHeader != null && authHeader.startsWith(tokenHead)) {
            String authToken = authHeader.substring(tokenHead.length());
            if (jwtService.validateToken(authToken)) {
                Claims claimsFromToken = jwtService.getClaimsFromToken(authToken);
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(claimsFromToken.getSubject(), null, null);
                String  userDTO = claimsFromToken.get("userDTO", String.class);
                log.info("=======lanjie={}",userDTO);
                request.setAttribute("userInfoDTO", userDTO);
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        chain.doFilter(request, response);
    }
}

解释:

  1. 正常实现Filter,然后对token进行解析验证
  2. 将UsernamePasswordAuthenticationToken放入SecurityContext即可

至此整个用户名密码验证流程结束

四、根据用户名密码模式思考短信验证码模式

我们了解了用户名密码整体流程之后,再来看如果是短信验证码该怎么做

我们来模仿整个看看思路:

  1. 写一个短信的Token跟UsernamePasswordAuthenticationToken,用于设置一些用户名等参数
  2. 写一个provider去获取用户信息,并进行相关的校验

然后其它的就跟用户名密码一样了,,

1、编写SmsAuthonticationToken继承AbstractAuthenticationToken(直接复制UsernamePasswordAuthonticationToken将里边的密码参数删掉就可以了)

详情可看源码:com.murphy.security.token.SmsAuthenticationToken

2、编写provider实现AuthenticationProvider

public class SmsAuthenticationProvider implements AuthenticationProvider {

//    private UserDetailsService userDetailsService;
    private UserExtendService userExtendService;
    public SmsAuthenticationProvider(){}

    public SmsAuthenticationProvider( UserExtendService userExtendService){
//        this.userDetailsService=userDetailsService;
        this.userExtendService = userExtendService;
    }

	@Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
        /**
         * 调用 {@link UserDetailsService}
         */
        UserDetails user = userExtendService.loadUserByUsername((String)authenticationToken.getPrincipal());
        if (Objects.isNull(user)) {
            throw new InternalAuthenticationServiceException("手机号不存在");
        }
        if (!userExtendService.validate(authenticationToken.getRequest())){
            throw new InternalAuthenticationServiceException("验证码错误");
        }
        SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(user, user.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

	@Override
	public boolean supports(Class<?> authentication) {
		return SmsAuthenticationToken.class.isAssignableFrom(authentication);
	}

}

这里主要看userExtendService.validate(authenticationToken.getRequest())

验证码校验属于业务,有各种各样的实现方案(比如放redis里)这里可以提供接口,让使用方实现接口,我们在这里调用一下

public interface UserExtendService extends UserDetailsService{
    /**
     * 业务校验
     * @param request
     * @return
     */
    default boolean validate(HttpServletRequest request){
        return true;
    };
}

然后再根据用户名密码那套流程,将usernamePasswordAuthenticationToken换成SmsAuthenticationToken;将SmsAuthenticationProvider放入配置即可

具体配置文件可看源码:com.murphy.security.config.WebSecurityConfig

这样如果再有其它的认证方式,画瓢即可,验证可以给默认,然后再暴露给使用者,可以重写

源码地址:https://gitee.com/carpentor/spring-cloud-example

公众号主要记录各种源码、面试题、微服务技术栈,帮忙关注一波,非常感谢

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章