Spring Security Oauth2资源服务器Starter配置

首先开下starter的结构:

|
|——-oauth2
      |
      |---auth-center-provider    认证中心
      |
      |---auth-provider-api         共用
      |
      |---auth-spring-boot-autoconifg   auto
      |
      |---auth-spring-boot-starter    starter

认证中心这里不说,后面有时间详细的进行讲解。

api模块,

Pom文件配置

    <description>授权中心API结构图</description>
    <dependencies>
        <!--security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.security.oauth/spring-security-oauth2 -->
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.codeus</groupId>
            <artifactId>saas-user</artifactId>
            <version>${project.version}</version>
            <scope>compile</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-jwt -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
            <version>1.0.9.RELEASE</version>
        </dependency>
    </dependencies>

BaseUserDetail

/**
 * 包装org.springframework.security.core.userdetails.User类
 * @author 大仙
 *
 */
public class BaseUserDetail implements UserDetails, CredentialsContainer {
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	/**
	 * 用户
	 */
	private final BaseUser baseUser;
	private final User user;

	public BaseUserDetail(BaseUser baseUser, User user) {
		this.baseUser = baseUser;
		this.user = user;
	}

	@Override
	public void eraseCredentials() {
		user.eraseCredentials();
	}

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return user.getAuthorities();
	}

	@Override
	public String getPassword() {
		return user.getPassword();
	}

	@Override
	public String getUsername() {
		return user.getUsername();
	}

	@Override
	public boolean isAccountNonExpired() {
		return user.isAccountNonExpired();
	}

	@Override
	public boolean isAccountNonLocked() {
		return user.isAccountNonLocked();
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return user.isCredentialsNonExpired();
	}

	@Override
	public boolean isEnabled() {
		return user.isEnabled();
	}

	public BaseUser getBaseUser() {
		return baseUser;
	}
}

TokenEntity

/**
 * token存储实体
 * @author 大仙
 */
@Data
public class TokenEntity implements Serializable {

    private String token;

    private LocalDateTime invalidDate;
}

异常配置:

/**
 * 权限相关异常
 */
public class AuthException extends SasException {

    protected Integer code;
    protected String title = "WeCode SAS Exception";

    public AuthException(String message) {
        super(message);
    }

    public AuthException(String message, int code) {
        super(message);
        this.code = code;
    }

    public AuthException(String message, int code, String title) {
        super(message);
        this.code = code;
        this.title = title;
    }

    public Integer getCode() {
        return code;
    }

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

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}

JwtAccessToken

扫描二维码关注公众号,回复: 8609700 查看本文章
public class JwtAccessToken extends JwtAccessTokenConverter{
	
	 /**
     * 生成token
     * @param accessToken
     * @param authentication
     * @return
     */
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        DefaultOAuth2AccessToken defaultOAuth2AccessToken = new DefaultOAuth2AccessToken(accessToken);

        // 设置额外用户信息
        if(authentication.getPrincipal() instanceof BaseUserDetail) {
	        BaseUser baseUser = ((BaseUserDetail) authentication.getPrincipal()).getBaseUser();
            baseUser.setPassword(null);
	        // 将用户信息添加到token额外信息中
	        defaultOAuth2AccessToken.getAdditionalInformation().put(Constant.USER_INFO, baseUser);
        }
        return super.enhance(defaultOAuth2AccessToken, authentication);
    }

    /**
     * 解析token
     * @param value
     * @param map
     * @return
     */
    @Override
    public OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map){
        OAuth2AccessToken oauth2AccessToken = super.extractAccessToken(value, map);
        convertData(oauth2AccessToken, oauth2AccessToken.getAdditionalInformation());
        return oauth2AccessToken;
    }

    private void convertData(OAuth2AccessToken accessToken,  Map<String, ?> map) {
        accessToken.getAdditionalInformation().put(Constant.USER_INFO,convertUserData(map.get(Constant.USER_INFO)));

    }

    private BaseUser convertUserData(Object map) {
        String json = JsonUtils.deserializer(map);
        BaseUser user = JsonUtils.serializable(json, BaseUser.class);
        return user;
    }
}

util

public class JsonUtils {
	private static ObjectMapper mapper = new ObjectMapper();

	public JsonUtils() {
	}

	public static <T> T serializable(String json, Class<T> clazz) {
		if (StringUtils.isEmpty(json)) {
			return null;
		} else {
			try {
				return mapper.readValue(json, clazz);
			} catch (IOException var3) {
				return null;
			}
		}
	}

	public static <T> T serializable(String json, TypeReference<T> reference) {
		if (StringUtils.isEmpty(json)) {
			return null;
		} else {
			try {
				return mapper.readValue(json, reference);
			} catch (IOException var3) {
				return null;
			}
		}
	}

	public static String deserializer(Object json) {
		if (json == null) {
			return null;
		} else {
			try {
				return mapper.writeValueAsString(json);
			} catch (JsonProcessingException var2) {
				return null;
			}
		}
	}

	static {
		mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
	}
}

TokenUtil

/**
 * token控制工具类
 * @author 大仙
 */
public class TokenUtil {
    /**
     * 存储token
     * @param telephone
     * @param redisTemplate
     * @param token
     * @return
     */
    public static Boolean pushToken(String telephone, RedisTemplate<String, TokenEntity> redisTemplate, String token, Date invalid){
        LocalDateTime invalidDate = invalid.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
        long size = redisTemplate.opsForList().size(telephone);
        TokenEntity tokenEntity = new TokenEntity();
        tokenEntity.setInvalidDate(invalidDate);
        tokenEntity.setToken(token);
        if(size<=0){
            redisTemplate.opsForList().rightPush(telephone,tokenEntity);
        }else{
            List<TokenEntity> tokenEntities = redisTemplate.opsForList().range(telephone, 0, size);
            tokenEntities = tokenEntities.stream().filter(te -> te.getInvalidDate().isAfter(LocalDateTime.now())).collect(Collectors.toList());
            if(tokenEntities.size()>= Constant.MAX_LOGIN){
                return false;
            }
            tokenEntities.add(tokenEntity);
            redisTemplate.delete(telephone);
            tokenEntities.forEach(te->{
                redisTemplate.opsForList().rightPush(telephone,te);
            });
        }
        return true;
    }

    /**
     * 判断token是否有效
     * @param telephone
     * @param redisTemplate
     * @param token
     * @return true 有效 false: 无效
     */
    public static Boolean judgeTokenValid(String telephone, RedisTemplate<String, TokenEntity> redisTemplate, String token){
        long size = redisTemplate.opsForList().size(telephone);
        if(size<=0){
            return false;
        }else{
            List<TokenEntity> tokenEntities = redisTemplate.opsForList().range(telephone, 0, size);
            tokenEntities = tokenEntities.stream().filter(te->te.getToken().equals(token)).collect(Collectors.toList());
            if(CollectionUtils.isEmpty(tokenEntities)){
                return false;
            }
            TokenEntity tokenEntity = tokenEntities.get(0);
            if(tokenEntity.getInvalidDate().isAfter(LocalDateTime.now())){
                return true;
            }
        }
        return false;
    }

    /**
     * 登出
     * @param telephone
     * @param redisTemplate
     * @param token
     */
    public static void logout(String telephone, RedisTemplate<String, TokenEntity> redisTemplate, String token){
        long size = redisTemplate.opsForList().size(telephone);
        if(size<=0){
            redisTemplate.delete(telephone);
        }else{
            List<TokenEntity> tokenEntities = redisTemplate.opsForList().range(telephone, 0, size);
            tokenEntities = tokenEntities.stream().filter(te->!te.getToken().equals(token)).collect(Collectors.toList());
            if(CollectionUtils.isEmpty(tokenEntities)){
                redisTemplate.delete(telephone);
            }
            redisTemplate.delete(telephone);
            tokenEntities.forEach(te->{
                redisTemplate.opsForList().rightPush(telephone,te);
            });
        }
    }
}

autoconfig模块:

/**
 * 权限管理
 *
 * @author 大仙
 */
@ConfigurationProperties(prefix = "security")
public class AccessDecisionManagerIml implements AccessDecisionManager {

    private Logger logger = LoggerFactory.getLogger(AccessDecisionManagerIml.class);
    @Autowired
    private AccessTokenUtils accessTokenUtils;

    private AntPathMatcher matcher = new AntPathMatcher();

    private String[] ignoreds;

    @Autowired
    private RedisTemplate<String, TokenEntity> redisTemplate;

    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        // 请求路径
        String url = getUrl(o);
        logger.info("请求URL:" + url);
        // http 方法
        String httpMethod = getMethod(o);
        logger.info("请求类型:" + httpMethod);
        //options的方法全部放行
        if (httpMethod.equals(HttpMethod.OPTIONS.name())) {
            return;
        }
        logger.info("白名单:"+Arrays.toString(ignoreds));
        // 不拦截的请求
        for (String path : ignoreds) {
            String temp = path.trim();
            if (matcher.match(temp, url)) {
                return;
            }
        }
        if(!TokenUtil.judgeTokenValid(accessTokenUtils.getUserInfo().getTelephone(),redisTemplate,accessTokenUtils.getAccessToken().getValue())){
            throw new AccessDeniedException("无权限!");
        }
        // URL 鉴权
        Iterator<String> iterator = accessTokenUtils.getRoleInfo().iterator();
        logger.info("url:"+url);
        if(iterator!=null){
            return;
        }
        logger.error("请求失败:url={},method={}",url,httpMethod);
        throw new AccessDeniedException("无权限!");

    }

    /**
     * 获取请求中的url
     */
    private String getUrl(Object o) {
        //获取当前访问url
        String url = ((FilterInvocation) o).getRequestUrl();
        int firstQuestionMarkIndex = url.indexOf("?");
        if (firstQuestionMarkIndex != -1) {
            return url.substring(0, firstQuestionMarkIndex);
        }
        return url.trim();
    }

    private String getMethod(Object o) {
        return ((FilterInvocation) o).getRequest().getMethod();
    }


    private boolean matchUrl(String url, String modulePath) {

        List urls = Arrays.asList(url.split("/")).stream().filter(e -> !"".equals(e)).collect(Collectors.toList());
        Collections.reverse(urls);

        List paths = Arrays.asList(modulePath.split("/")).stream().filter(e -> !"".equals(e)).collect(Collectors.toList());
        Collections.reverse(paths);

        // 如果数量不相等
        if (urls.size() != paths.size()) {
            return false;
        }

        for (int i = 0; i < paths.size(); i++) {
            // 如果是 PathVariable 则忽略
            String item = (String) paths.get(i);
            if (item.charAt(0) != '{' && item.charAt(item.length() - 1) != '}') {
                // 如果有不等于的,则代表 URL 不匹配
                if (!item.equals(urls.get(i))) {
                    return false;
                }
            }
        }
        return true;
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }

    public void setIgnored(String ignored) {
        if (ignored != null && !"".equals(ignored)) {
            this.ignoreds = ignored.split(",");
        } else {
            this.ignoreds = new String[]{};
        }
    }
}
/**
 * token工具类
 * @author 大仙
 */
public class AccessTokenUtils {

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private HttpServletRequest request;

    @Autowired
    private TokenExtractor tokenExtractor;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;


    /**
     * 从token获取用户信息
     * @return
     */
    public BaseUser getUserInfo(){
        BaseUser baseUser =  (BaseUser) getAccessToken().getAdditionalInformation().get(Constant.USER_INFO);
        if(baseUser==null){
            throw new AccessDeniedException("无效TOKEN");
        }
        return baseUser;
    }

    /**
     * 获取TOEKN
     * @return
     * @throws AccessDeniedException
     */
    public OAuth2AccessToken getAccessToken() throws AccessDeniedException {
        OAuth2AccessToken token;
        // 抽取token
        Authentication a = tokenExtractor.extract(request);
        try {
            // 调用JwtAccessTokenConverter的extractAccessToken方法解析token
            token = tokenStore.readAccessToken((String) a.getPrincipal());
        } catch(Exception e) {
            throw new AccessDeniedException("AccessToken Not Found.");
        }
        return token;
    }

    /**
     * 获取用户角色信息
     * @return
     */
    public List<String> getRoleInfo(){
        String userId = String.valueOf(getUserInfo().getId());
        long size = redisTemplate.opsForList().size(userId);
        return redisTemplate.opsForList().range(userId, 0, size);
    }

}
/**
 * 资源服务配置
 * @ EnableResourceServer 启用资源服务
 * @ EnableWebSecurity 启用web安全
 * @ EnableGlobalMethodSecurity 启用全局方法安全注解,就可以在方法上使用注解来对请求进行过滤
 * @author 大仙
 */
@Configuration
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {


    private static final String PUBLIC_KEY = "pubkey.txt";


    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(resJwtAccessTokenConverter());
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        //禁用csrf
        http.csrf().disable()
                .formLogin().permitAll()
                .and().httpBasic();
        // 解决不允许显示在iframe的问题
        http.headers().frameOptions().sameOrigin();
         //拦截所有请求,下面2种写法都行
        http.authorizeRequests().antMatchers("/**").authenticated();
    }

    @Bean
    public JwtAccessTokenConverter resJwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessToken();
        String publicKey = getPubKey();
        converter.setVerifierKey(publicKey);
        //不设置这个会出现 Cannot convert access token to JSON
        converter.setVerifier(new RsaVerifier(publicKey));
        return converter;
    }
    /**
     * 获取非对称加密公钥 Key
     * @return 公钥 Key
     */
    private String getPubKey() {
        Resource resource = new ClassPathResource(PUBLIC_KEY);
        try (BufferedReader br = new BufferedReader(new InputStreamReader(resource.getInputStream()))) {
            return br.lines().collect(Collectors.joining("\n"));
        } catch (IOException ioe) {
            throw new AuthException("查询公钥出错");
        }
    }
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        super.configure(resources);
    }

}
/**
 * 配置
 * @author 大仙
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@Order(1)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        // 基于token,所以不需要session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http    // 配置授权管理器
                .authorizeRequests().accessDecisionManager(getAccessDecisionManager())
                // 匹配全部请求鉴权认证
                .and().authorizeRequests().anyRequest().authenticated()
                // 由于使用的是JWT,我们这里不需要csrf
                .and().csrf().disable();
    }

    @Bean
    public AccessDecisionManager getAccessDecisionManager() {
        return new AccessDecisionManagerIml();
    }

    @Bean
    public TokenExtractor getTokenExtractor() {
        return new BearerTokenExtractor();
    }

}

/**
 * redis配置
 * @author 大仙
 *
 */
@Configuration
public class RedisConfig {

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public RedisTemplate<String, TokenEntity> tokenEntityRedisTemplate() {
        RedisTemplate<String, TokenEntity> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new RedisObjectSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }

}
/**
 * redis编码解码类
 * @Author: 朱维
 * @Date 17:38 2019/11/27
 */
public class RedisObjectSerializer implements RedisSerializer {

    static final byte[] EMPTY_ARRAY = new byte[0];

    private Converter<Object, byte[]> serializer = new SerializingConverter();
    private Converter<byte[], Object> deserializer = new DeserializingConverter();

    @Override
    public byte[] serialize(Object o) throws SerializationException {
        if(o == null) {
            return EMPTY_ARRAY;
        }
        try {
            return serializer.convert(o);
        }catch (Exception e){
            return EMPTY_ARRAY;
        }

    }

    @Override
    public Object deserialize(byte[] bytes) throws SerializationException {
        if(isEmpty(bytes))
            return null;
        try {
            return deserializer.convert(bytes);
        }catch (Exception e){
            throw new SerializationException("Cannot deserialize", e);
        }
    }

    private boolean isEmpty(byte[] bytes){
        return (bytes == null || bytes.length == 0) ;
    }
}
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.codeus.auth.config.ResourceServerConfig,\
com.codeus.auth.config.AccessDecisionManagerIml,\
com.codeus.auth.config.AccessTokenUtils,\
com.codeus.auth.config.WebSecurityConfig,\
com.codeus.auth.redis.RedisConfig

starter模块

 spring.provider:

provides: auth-spring-boot-autoconfigure

应用服务引用即可:

 <dependency>
            <groupId>com.codeus</groupId>
            <artifactId>auth-spring-boot-starter</artifactId>
            <version>${project.version}</version>
        </dependency>

无需其他的配置

发布了149 篇原创文章 · 获赞 36 · 访问量 14万+

猜你喜欢

转载自blog.csdn.net/zhuwei_clark/article/details/103980661