springcloud+gateway(zuul)+springsecurity+jwt实现简单的认证授权

前段时间一直在研究微服务的认证和授权的方式,网上给了大致4种模式,感觉配置起来都不是很得心应手,偶然间看到了一个简单且较为完整的jwt+springsecurity的配置方式,这里先给出参考的github上的源码:

https://github.com/shuaicj/zuul-auth-example

但跟着配置后,问题还是很多,套用到自己的微服务框架上还是有些难度.

 

github工程包里有4个核心的类

common下的

springcloud+gateway(zuul)+springsecurity+jwt实现简单的认证授权_第1张图片

认证中心和网关下各有一个security的配置类

 
  

简单解释下这些类都是干什么的,就不粘代码了,github上有....

springcloud+gateway(zuul)+springsecurity+jwt实现简单的认证授权_第2张图片

 

springcloud+gateway(zuul)+springsecurity+jwt实现简单的认证授权_第3张图片

springcloud+gateway(zuul)+springsecurity+jwt实现简单的认证授权_第4张图片

这个是auth下的securityconfig

springcloud+gateway(zuul)+springsecurity+jwt实现简单的认证授权_第5张图片

gateway下的securityconfig

springcloud+gateway(zuul)+springsecurity+jwt实现简单的认证授权_第6张图片

这里总结下修改的不同点,以及踩的坑:

1.auth下的securityconfig采用的内存存储模式

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
    auth.inMemoryAuthentication()
            .withUser("admin").password(encoder.encode("admin")).roles("ADMIN", "USER").and()
            .withUser("shuaicj").password(encoder.encode("shuaicj")).roles("USER");
}

所以我们应该修改为数据库读取的模式

service层的这个loadUserByUsername用于根据登录时输入的账号来获取用户并得到权限

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username)  {
        if(StringUtils.isEmpty(username)) {
            throw new UsernameNotFoundException("UserDetailsService没有接收到用户账号");
        } else {
           
            Example example = new Example(User.class);
            Example.Criteria criteria = example.createCriteria();
            criteria.andEqualTo("username",username);
            List users = userMapper.selectByExample(example);
            if(users == null) {
                throw new UsernameNotFoundException(String.format("用户'%s'不存在", username));
            }
            User user = users.get(0);
            List roles = userMapper.findRoles(user.getId());
            List grantedAuthorities = new ArrayList<>();
            for (Role role : roles) {
                //封装用户信息和角色信息到SecurityContextHolder全局缓存中
                grantedAuthorities.add(new SimpleGrantedAuthority(role.getRole()));
                System.out.println(role);
            }
           
            return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), grantedAuthorities);
        }
    }
}

这里就将原来的内存存储的机制修改为数据库后的方式

@Autowired
protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        /**
         * 指定用户认证时,默认从哪里获取认证用户信息
         */
        auth.userDetailsService(userDetailsServiceImpl).passwordEncoder(new BCryptPasswordEncoder());

}

 

然后我们调用登录接口,这个路径在yml下配置....

springcloud+gateway(zuul)+springsecurity+jwt实现简单的认证授权_第7张图片

然后把header中的token,拿去调用服务下的接口

springcloud+gateway(zuul)+springsecurity+jwt实现简单的认证授权_第8张图片

然后就爆炸了....因为我数据库里这个用户是有权限的...但还是被禁止了...

折腾了一上午,终于跳出了坑,

这是正确的角色表存储方法

之前我一直是USER,ADMIN...因为在springsecurity下它的授权都是以ROLE_XXXX,而之前读取的是XXX,不匹配就禁止了,当然不想改数据库,可以在分配权限的地方都加上ROLE_,一样的效果.

2.获取登录获取token的方式,感觉不是很优雅,之前做项目都是有一个result返回类的,他这个登录后直接在header里加,感觉不是很习惯...

修改了下这个成功认证的方法,写了一个controller自定义了下返回集,把token带过去

@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse rsp, FilterChain chain,
                                        Authentication auth) throws IOException {
    Instant now = Instant.now();
    String token = Jwts.builder()
            .setSubject(auth.getName())
            .claim("authorities", auth.getAuthorities().stream()
                    .map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
            .setIssuedAt(Date.from(now))
            .setExpiration(Date.from(now.plusSeconds(config.getExpiration())))
            .signWith(SignatureAlgorithm.HS256, config.getSecret().getBytes())
            .compact();
    rsp.sendRedirect("/authSuccess?token="+config.getPrefix() + " " + token);
}

在用postman测试就有了自己想要的返回结果

springcloud+gateway(zuul)+springsecurity+jwt实现简单的认证授权_第9张图片

3.模拟验证码,github工程里的登录是没有验证码的...想加入自己的验证方式,模拟下

这里同理重定向转发到自定义的返回集...然后在中间模拟checkCode

@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse rsp)
        throws AuthenticationException, IOException {
    User u = null;
    try{
        u = mapper.readValue(req.getInputStream(),User.class);
        if (!u.getCheck().equals("success")) {
            rsp.sendRedirect("/authError?message=checkCodeError");
        } else {
            return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(
                    u.getUsername(), u.getPassword(), Collections.emptyList()
            ));
        }
    }catch (UnrecognizedPropertyException e) {
        e.printStackTrace();
        System.out.println(e);
        rsp.sendRedirect("/authError?message=authParamsError");
    }
    return null;
}

这个就是重定向的controller,根据自己的需求更改返回集

@RestController

public class AuthController {

    @RequestMapping("/authSuccess")
    public Result authSuccess(@PathParam("token") String token) {
        return Result.ok(token);
    }

    @RequestMapping("/authError")
    public Result authError(@PathParam("message") String message) {
        return Result.error(401,message);
    }
}

4.在微服务zuul其他的service中请求头消失

这是我一个微服务想解析zuul转发的header中的token,拿到用户名和角色..然后就一直为空.........

 

@DeleteMapping("/admin")
public Result admin(HttpServletRequest request) {
    request.getHeaderNames();
    String username = JwtUtil.getUsername(request);
    List authorities = JwtUtil.getAuthorities(request);
    System.out.println(username);
    for(String s : authorities) {
        System.out.println(s);
    }
    return  Result.ok("admin");
}

找了网上的一些说法

说在网关的yml配置下这个就ok了,然并卵..

网上有人说配置下这个:

RequestContext context = RequestContext.getCurrentContext(); context.addZuulRequestHeader("Authorization",req.getHeader(config.getHeader()));

于是我便想到了在jwttoken认证过滤类中添加

public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {

    private final JwtAuthenticationConfig config;

    public JwtTokenAuthenticationFilter(JwtAuthenticationConfig config) {
        this.config = config;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse rsp, FilterChain filterChain)
            throws ServletException, IOException {
        String token = req.getHeader(config.getHeader());
        if (token != null && token.startsWith(config.getPrefix() + " ")) {
            token = token.replace(config.getPrefix() + " ", "");
            try {
                Claims claims = Jwts.parser()
                        .setSigningKey(config.getSecret().getBytes())
                        .parseClaimsJws(token)
                        .getBody();
                String username = claims.getSubject();
                @SuppressWarnings("unchecked")
                List authorities = claims.get("authorities", List.class);

                if (username != null) {
                    UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null,
                            authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()));
                    SecurityContextHolder.getContext().setAuthentication(auth);
                    RequestContext context = RequestContext.getCurrentContext();
                    context.addZuulRequestHeader("Authorization",req.getHeader(config.getHeader()));
                }
            } catch (Exception ignore) {
                SecurityContextHolder.clearContext();
            }
        }
        filterChain.doFilter(req, rsp);
    }
}

最终可以微服务中拿到用户信息....

大概就修改了这么多,基本满足了自己微服务的一些要求,实际上这种认证授权并没有真正意义上的授权,在网关通过路径来对各个微服务进行拦截,可能会对增大网关的压力,有待后期考察.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(java,#,springcloud,#,springsecurity)