2万字带你从0到1搭建一套企业级微服务安全框架_从 0 到 框架 到企业级管理系统

    @PostMapping("/login")
    @AnonymousAccess
    public ResponseEntity<Object> login(@Validated @RequestBody AuthUserDto authUser){
        // 密码解密
// String password = RsaUtils.decryptByPrivateKey(RsaProperties.privateKey, authUser.getPassword());
        String password = authUser.getPassword();
        // 将用户名、密码、封装成UsernamePasswordAuthenticationToken对象
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(authUser.getUsername(), password);
        // 获取认证管理器
        AuthenticationManager authenticationManager = authenticationManagerBuilder.getObject();
        // 认证核心方法
        Authentication authentication = authenticationManager.authenticate(authenticationToken);
// // 认证成功之后,将认证信息保存至SecurityContext中
// SecurityContextHolder.getContext().setAuthentication(authentication);
        // 根据认证信息生成token
        String token = tokenProvider.createToken(authentication);
        // 获取用户身份信息
        User one = userService.getOne(new QueryWrapper<User>().eq("username", authUser.getUsername()));
        UserDto userDto = new UserDto();
        BeanUtils.copyProperties(one,userDto);

        stringRedisTemplate.opsForValue().set(properties.getOnlineKey() + token, JSONUtil.toJsonStr(userDto), properties.getTokenValidityInSeconds()/1000, TimeUnit.SECONDS);

        // 返回 token 与 用户信息
        Map<String, Object> authInfo = new HashMap<String, Object>(2) {{
            put("token", properties.getTokenStartWith() + token);
            put("user", userDto);
        }};
        return ResponseEntity.ok(authInfo);
    }

package com.ossa.system.filter;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ossa.common.api.bean.Privilege;
import com.ossa.common.api.bean.Role;
import com.ossa.common.api.bean.User;
import com.ossa.system.mapper.PrivilegeMapper;
import com.ossa.system.mapper.RoleMapper;
import com.ossa.system.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import javax.persistence.EntityNotFoundException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@RequiredArgsConstructor
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserService userService;
    private final RoleMapper roleMapper;
    private final PrivilegeMapper privilegeMapper ;


    @Override
    public UserDetails loadUserByUsername(String username) {
        User user;
        org.springframework.security.core.userdetails.User userDetails;
        try {
            user = userService.getOne(new QueryWrapper<User>().eq("username", username));

        } catch (EntityNotFoundException e) {
            // SpringSecurity会自动转换UsernameNotFoundException为BadCredentialsException
            throw new UsernameNotFoundException("", e);
        }
        if (user == null) {
            throw new UsernameNotFoundException("");
        } else {

            List<Role> roles = roleMapper.listByUserId(user.getId());

            ArrayList<Privilege> privileges = new ArrayList<>();

            roles.forEach(role -> privileges.addAll(privilegeMapper.listByRoleId(role.getId())));

            ArrayList<String> tag = new ArrayList<>();

            privileges.forEach(p -> tag.add(p.getTag()));

            List<SimpleGrantedAuthority> collect = tag.stream().map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
            userDetails = new org.springframework.security.core.userdetails.User(username, user.getPassword(), collect);

        }
        return userDetails;
    }
}

package com.ossa.system.filter;


import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import com.ossa.common.bean.SecurityProperties;
import io.jsonwebtoken.\*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.security.Key;
import java.util.ArrayList;
import java.util.Date;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component
public class TokenProvider implements InitializingBean {

    private final SecurityProperties properties;
    private final StringRedisTemplate stringRedisTemplate;
    public static final String AUTHORITIES_KEY = "user";
    private JwtParser jwtParser;
    private JwtBuilder jwtBuilder;

    public TokenProvider(SecurityProperties properties, StringRedisTemplate stringRedisTemplate) {
        this.properties = properties;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public void afterPropertiesSet() {
        byte[] keyBytes = Decoders.BASE64.decode(properties.getBase64Secret());
        Key key = Keys.hmacShaKeyFor(keyBytes);
        jwtParser = Jwts.parserBuilder()
                .setSigningKey(key)
                .build();
        jwtBuilder = Jwts.builder()
                .signWith(key, SignatureAlgorithm.HS512);
    }

    /\*\*
 \* 创建Token 设置永不过期,
 \* Token 的时间有效性转到Redis 维护
 \*
 \* @param authentication /
 \* @return /
 \*/
    public String createToken(Authentication authentication) {
        return jwtBuilder
                // 加入ID确保生成的 Token 都不一致
                .setId(IdUtil.simpleUUID())
                .claim(AUTHORITIES_KEY, authentication.getName())
                .setSubject(authentication.getName())
                .compact();
    }

    /\*\*
 \* 依据Token 获取鉴权信息
 \*
 \* @param token /
 \* @return /
 \*/
    Authentication getAuthentication(String token) {
        Claims claims = getClaims(token);
        User principal = new User(claims.getSubject(), "\*\*\*\*\*\*", new ArrayList<>());
        return new UsernamePasswordAuthenticationToken(principal, token, new ArrayList<>());
    }

    public Claims getClaims(String token) {
        return jwtParser
                .parseClaimsJws(token)
                .getBody();
    }

    /\*\*
 \* @param token 需要检查的token
 \*/
    public void checkRenewal(String token) {
        // 判断是否续期token,计算token的过期时间
        Long expire = stringRedisTemplate.getExpire(properties.getOnlineKey() + token, TimeUnit.SECONDS);
        long time = expire == null ? 0 : expire \* 1000;
        Date expireDate = DateUtil.offset(new Date(), DateField.MILLISECOND, (int) time);
        // 判断当前时间与过期时间的时间差
        long differ = expireDate.getTime() - System.currentTimeMillis();
        // 如果在续期检查的范围内,则续期
        if (differ <= properties.getDetect()) {
            long renew = time + properties.getRenew();
            stringRedisTemplate.expire(properties.getOnlineKey() + token, renew, TimeUnit.MILLISECONDS);
        }
    }

    public String getToken(HttpServletRequest request) {
        final String requestHeader = request.getHeader(properties.getHeader());
        if (requestHeader != null && requestHeader.startsWith(properties.getTokenStartWith())) {
            return requestHeader.substring(7);
        }
        return null;
    }
}

授权设计

  1. 设计自己filter,拦截我们生成的token,如果token合法,则将token解析并封装成UsernamePasswordAuthenticationToken,存到安全上下文中
  2. 为了确保授权成功,我们需要将我们的filter放在UsernamePasswordAuthenticationFilter前执行
package com.ossa.system.filter;

import cn.hutool.core.util.StrUtil;
import com.ossa.common.bean.SecurityProperties;
import io.jsonwebtoken.ExpiredJwtException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class OssaTokenFilter extends GenericFilterBean {
    private static final Logger log = LoggerFactory.getLogger(OssaTokenFilter.class);

    private final StringRedisTemplate stringRedisTemplate;

    private final TokenProvider tokenProvider;
    private final SecurityProperties properties;

    /\*\*
 \* @param tokenProvider Token
 \* @param properties JWT
 \*/
    public OssaTokenFilter(TokenProvider tokenProvider, SecurityProperties properties, StringRedisTemplate stringRedisTemplate) {
        this.properties = properties;
        this.tokenProvider = tokenProvider;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        String token = resolveToken(httpServletRequest);
        // 对于 Token 为空的不需要去查 Redis
        if (StrUtil.isNotBlank(token)) {
            String s = null;
            try {
                s = stringRedisTemplate.opsForValue().get(properties.getOnlineKey() + token);
            } catch (ExpiredJwtException e) {
                log.error(e.getMessage());
            }
            if (s != null && StringUtils.hasText(token)) {
                Authentication authentication = tokenProvider.getAuthentication(token);
                SecurityContextHolder.getContext().setAuthentication(authentication);
                // Token 续期
                tokenProvider.checkRenewal(token);
            }
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }

    /\*\*
 \* 初步检测Token
 \*
 \* @param request /
 \* @return /
 \*/
    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(properties.getHeader());
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(properties.getTokenStartWith())) {
            // 去掉令牌前缀
            return bearerToken.replace(properties.getTokenStartWith(), "");
        } else {
            log.debug("非法Token:{}", bearerToken);
        }
        return null;
    }
}


核心配置

package com.ossa.common.security.core.config;

import com.ossa.common.api.anno.AnonymousAccess;
import com.ossa.common.api.bean.SecurityProperties;
import com.ossa.common.api.enums.RequestMethodEnum;
import com.ossa.common.security.core.filter.OssaTokenFilter;
import com.ossa.common.security.core.filter.TokenProvider;
import com.ossa.common.security.core.handler.JwtAccessDeniedHandler;
import com.ossa.common.security.core.handler.JwtAuthenticationEntryPoint;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.util.\*;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class OssaSecurityConfigurer extends WebSecurityConfigurerAdapter {
    private final TokenProvider tokenProvider;
    private final SecurityProperties properties;
    private final ApplicationContext applicationContext;
    private final JwtAuthenticationEntryPoint authenticationErrorHandler;

    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;

    private final StringRedisTemplate stringRedisTemplate;

    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    GrantedAuthorityDefaults grantedAuthorityDefaults() {
        // 去除 ROLE\_ 前缀
        return new GrantedAuthorityDefaults("");
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        // 密码加密方式
        return new BCryptPasswordEncoder();

    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {

        OssaTokenFilter customFilter = new OssaTokenFilter(tokenProvider, properties,stringRedisTemplate);

        // 搜寻匿名标记 url: @AnonymousAccess
        RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping");
        Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();
        // 获取匿名标记
        Map<String, Set<String>> anonymousUrls = getAnonymousUrl(handlerMethodMap);
        httpSecurity
                // 禁用 CSRF
                .csrf().disable()
                .addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class)

                // 授权异常
                .exceptionHandling()
                .authenticationEntryPoint(authenticationErrorHandler)
                .accessDeniedHandler(jwtAccessDeniedHandler)
                // 防止iframe 造成跨域
                .and()
                .headers()
                .frameOptions()
                .disable()
                // 不创建会话
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 静态资源等等
                .antMatchers(
                        HttpMethod.GET,
                        "/\*.html",
                        "/\*\*/\*.html",
                        "/\*\*/\*.css",
                        "/\*\*/\*.js",
                        "/webSocket/\*\*"
                ).permitAll()
                // swagger 文档
                .antMatchers("/swagger-ui.html").permitAll()
                .antMatchers("/swagger-resources/\*\*").permitAll()
                .antMatchers("/webjars/\*\*").permitAll()
                .antMatchers("/\*/api-docs").permitAll()
                // 文件
                .antMatchers("/avatar/\*\*").permitAll()
                .antMatchers("/file/\*\*").permitAll()
                // 阿里巴巴 druid
                .antMatchers("/druid/\*\*").permitAll()
                // 放行OPTIONS请求
                .antMatchers(HttpMethod.OPTIONS, "/\*\*").permitAll()
                // 自定义匿名访问所有url放行:允许匿名和带Token访问,细腻化到每个 Request 类型
                // GET
                .antMatchers(HttpMethod.GET, anonymousUrls.get(RequestMethodEnum.GET.getType()).toArray(new String[0])).permitAll()
                // POST
                .antMatchers(HttpMethod.POST, anonymousUrls.get(RequestMethodEnum.POST.getType()).toArray(new String[0])).permitAll()
                // PUT
                .antMatchers(HttpMethod.PUT, anonymousUrls.get(RequestMethodEnum.PUT.getType()).toArray(new String[0])).permitAll()
                // PATCH
                .antMatchers(HttpMethod.PATCH, anonymousUrls.get(RequestMethodEnum.PATCH.getType()).toArray(new String[0])).permitAll()
                // DELETE
                .antMatchers(HttpMethod.DELETE, anonymousUrls.get(RequestMethodEnum.DELETE.getType()).toArray(new String[0])).permitAll()
                // 所有类型的接口都放行
                .antMatchers(anonymousUrls.get(RequestMethodEnum.ALL.getType()).toArray(new String[0])).permitAll()

                // 所有请求都需要认证
                .anyRequest().authenticated();
    }

    private Map<String, Set<String>> getAnonymousUrl(Map<RequestMappingInfo, HandlerMethod> handlerMethodMap) {
        Map<String, Set<String>> anonymousUrls = new HashMap<>(6);
        Set<String> get = new HashSet<>();
        Set<String> post = new HashSet<>();
        Set<String> put = new HashSet<>();
        Set<String> patch = new HashSet<>();
        Set<String> delete = new HashSet<>();
        Set<String> all = new HashSet<>();
        for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) {
            HandlerMethod handlerMethod = infoEntry.getValue();
            AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class);
            if (null != anonymousAccess) {
                List<RequestMethod> requestMethods = new ArrayList<>(infoEntry.getKey().getMethodsCondition().getMethods());
                RequestMethodEnum request = RequestMethodEnum.find(requestMethods.size() == 0 ? RequestMethodEnum.ALL.getType() : requestMethods.get(0).name());
                switch (Objects.requireNonNull(request)) {
                    case GET:
                        get.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
                        break;
                    case POST:
                        post.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
                        break;
                    case PUT:
                        put.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
                        break;
                    case PATCH:
                        patch.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
                        break;
                    case DELETE:
                        delete.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
                        break;
                    default:
                        all.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
                        break;
                }
            }
        }
        anonymousUrls.put(RequestMethodEnum.GET.getType(), get);
        anonymousUrls.put(RequestMethodEnum.POST.getType(), post);
        anonymousUrls.put(RequestMethodEnum.PUT.getType(), put);
        anonymousUrls.put(RequestMethodEnum.PATCH.getType(), patch);
        anonymousUrls.put(RequestMethodEnum.DELETE.getType(), delete);
        anonymousUrls.put(RequestMethodEnum.ALL.getType(), all);
        return anonymousUrls;
    }
}


自定义权限注解

package com.ossa.common.security.core.config;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@Service(value = "pc")
public class PermissionConfig {

    public Boolean check(String... permissions) {
        // 获取当前用户的所有权限
        List<String> permission = SecurityContextHolder.getContext()
                .getAuthentication()
                .getAuthorities()
                .stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors
                        .toList());
        // 判断当前用户的所有权限是否包含接口上定义的权限
        return permission.contains("ADMIN") || permission.contains("INNER") || permission.contains("OFFICEIT") || Arrays.stream(permissions).anyMatch(permission::contains);
    }
}

权限异常处理


package com.ossa.common.security.core.handler;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        //当用户在没有授权的情况下访问受保护的REST资源时,将调用此方法发送403 Forbidden响应
        response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage());
    }
}



package com.ossa.common.security.core.handler;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        // 当用户尝试访问安全的REST资源而不提供任何凭据时,将调用此方法发送401 响应
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException == null ? "Unauthorized" : authException.getMessage());
    }
}


网关处理

网关只需要转发token到具体服务即可

在写这篇文章之前,此部分我已经升级成UAA认证授权中心,故没有此处相关代码。

内部流量处理

在内部流量的设计过程中,我们并不需要网关分发的token,故在此设计时,我只在feign的api接口处统一增加权限标识,并经过简单加密。

并在上述的自定的权限注解处放过该标识,不进行权限校验。

package com.ossa.feign.config;

import com.ossa.feign.util.EncryptUtil;
序言 自从Martin Fowler对微服务作出定义之后,微服务便火遍大江南北, 网上出现很多文章来描述它的好处,也有很多文章来说明它的弊端。这便 让很多小伙伴无所适从,微服务究竟是什么,要不要使用微服务架构,怎 么实施微服务架构?我一直认为,微服务架构只是新瓶装老酒,这老酒就 是模块化。如果在做系统设计时,已经把模块化做得很好,转型微服务只 是顺理成章的事。如果模块化都做不好,转型微服务只会来灾难。 2014 年底,我们团队意识到 Docker 技术可以帮我们大幅度提高软 件产品的性能,降低硬件的投入,提高运维效率,便开始着手研发基于 Docker 的 PaaS 平台。随后,很快发现,PaaS 平台只是解决了软件生命周 期后半部分(运维)的问题,就思考能否通过 Docker 技术来提高开发团 队的效率。例如,降低团队成员流动来的风险,提高多团队协作的效率, 找到组件或知识积累的方法,让同一个软件产品能够适应不同客户的定制 化需求,等等。从此,就与微服务结下了不解之缘。这些目标确定后,通 用的PaaS平台的研发目标也就变成了解决以上问题的微服务平台的研发, 以及后来的青柳云平台本身的微服务化的实践。 在做微服务架构技术选型的时候,我们以“无侵入”和“社区活跃” 为最主要的考量点,也只有这样,将来在升级为原子服务架构、量子服务 架构的时候,甚至是恢复成单体架构的时候,代价才是最小的。所以,在 3 InfoQ 中文站 为数不多的可选项中,我们拥抱了 Spring Cloud。最后的结果就是使用 基于 Docker 的微服务平台进行开发和运行运维支撑,使用 Spring Cloud 进行业务系统开发,两者相互独立,并可被独立替换。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值