spring cloud难点:spring-security

这里主要介绍spring-cloud的难点问题,spring-security,以及吐槽

spring-security包含oauth2的部分是整个spring-cloud的难点

本人常年混迹外包,流浪n个公司,可无一个公司能做好登陆系统,野路子横行,代码杂乱霸道,领导得过且过,证明外包都是垃圾的话是可信的,我也无力反驳,但其实做一个登陆系统并非什么难事

oauth2并不困难,下面是个简单的例子

首先介绍关于密钥的几个命令:

openssl在linux机上执行,keytool在win机上执行,当然了先生成jre并加入
环境变量是必要的。

  1. 私钥:
openssl genrsa -out auth.server.key 4096
  1. 公钥:
openssl req -x509 -new -nodes -sha512 -days 3650 \
 -subj "/C=CN/ST=Beijing/L=Beijing/O=example/OU=Personal/CN=auth.server" \
 -key auth.server.key \
 -out auth.server.crt
  1. 生成p12文件,后缀可以是p12,pfx等,密码都用123456
openssl pkcs12 -export -in auth.server.crt -inkey auth.server.key -out auth.server.p12 -name "auth.server" 
  1. 导出信任证书
keytool -export -alias auth.server -auth.server.p12 -storepass 123456 -file auth.server.cer
  1. 信任证书导入trust.jks
keytool -importcert -keystore trust.jks -file auth.server.cer -alias auth.server -storepass 123456 -noprompt

理解这些命令之后,大部分ssl基本都很好配了

首先用spring-security写一个登陆逻辑

必要依赖,其他看着加:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-consul-config</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>

实体类(可以是两个,表建三个,其中一个是关联表):

@Data
public class User implements Serializable {

	private static final long serialVersionUID = 7826346293180917753L;
	
	private String userId;
	private String phone;
	private String username;
	private String password;
	private Boolean enabled;
	private Boolean accountNotExpired;
	private Boolean credentialsNonExpired;
	private Boolean accountNotLocked;
	
}
@Data
public class Role implements Serializable {

    private static final long serialVersionUID = -4625884686614740774L;

    private String roleId;
    private String name;
    private String alias;
    private String descr;

}

数据库查询

import java.util.Optional;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import sample.entity.User;

@Mapper
public interface UserRepository {

	@Insert("insert into `user`(user_id,username,password,phone,enabled,account_not_expired,"
			+ "credentials_non_expired,account_not_locked) values(#{userId,jdbcType=VARCHAR},"
			+ "#{username,jdbcType=VARCHAR},#{password,jdbcType=VARCHAR},#{enabled,jdbcType=BOOLEAN},"
			+ "#{account_not_expired,jdbcType=BOOLEAN},#{credentialsNonExpired,jdbcType=BOOLEAN},"
			+ "#{account_not_locked,jdbcType=BOOLEAN})")
	int saveUser(User user);

	@Select("select * from `user` where username=#{username,jdbcType=VARCHAR}")
	Optional<User> findByUsername(@Param("username") String username);

}
import java.util.List;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import sample.entity.Role;

@Mapper
public interface RoleRepository {

	@Insert("insert into `role`(`role_id`,`name`,`alias`,`descr`)values("
			+ "#{roleId,jdbcType=VARCHAR},#{name,jdbcType=VARCHAR},#{"
			+ "alias,jdbcType=VARCHAR},#{descr,jdbcType=VARCHAR})")
	int savaRole(Role role);
	
	@Select("select r.* from `role` r,`user_role` ur,`user` u "
			+ "where u.user_id=ur.user_id and r.role_id=ur.role_id "
			+ "and u.username=#{username,jdbcType=VARCHAR}")
	List<Role> findByUsername(@Param("username") String username);
}

数据库操作只是参考,自己随便写都行

security配置:

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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 sample.repository.RoleRepository;
import sample.repository.UserRepository;

@Service
public class CustomerUserDetailsService implements UserDetailsService {

	private @Autowired UserRepository userRepository;
	private @Autowired RoleRepository roleRepository;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		return userRepository.findByUsername(username).map(u -> {
			List<GrantedAuthority> authorities = roleRepository.findByUsername(username).stream().map(r -> {
				String role = r.getAlias();
				if (role.startsWith("ROLE_")) {
					return new SimpleGrantedAuthority(role);
				}
				return new SimpleGrantedAuthority("ROLE_" + role);
			}).collect(Collectors.toList());
			return new org.springframework.security.core.userdetails.User(username, u.getPassword(), u.getEnabled(),
					u.getAccountNotExpired(), true, u.getAccountNotLocked(), authorities);
		}).orElseThrow(() -> new UsernameNotFoundException("未找到用户: " + username));
	}

}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter{

    private @Autowired CustomerUserDetailsService userDetailsService;

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(this.userDetailsService);
    }

    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers(new String[] { "/.well-known/jwks.json", "/login", "/sms/login", "/sms/code" }).permitAll()
                .anyRequest().authenticated().and().formLogin().loginPage("/login").permitAll().defaultSuccessUrl("/")
                .and().logout().permitAll().and().csrf().disable();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(new String[] { "/css/**", "/img/**", "/js/**" });
    }

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

}

controller的写法:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class LoginPageController {

	@GetMapping("/login")
	public String loginPage() {
		return "loginPage";
	}

	@GetMapping("/")
	public String mainPage() {
		return "mainPage";
	}

}

前端代码(bootstrap依赖自己添加):

<!DOCTYPE html>
<html lang="zh-cn" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<title>Authorization Sample</title>
<link rel="stylesheet" th:href="@{/css/login.css}" />
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}" />
</head>
<body>
	<div class="container">
		<h3 align="center">这是一个带有登录框的主页</h3>
		<form class="form-signin" th:action="@{/login}" method="post">
			<h2 class="form-signin-heading">请 登 录</h2>
			<input type="text" class="form-control" placeholder="账号" name="username" /> 
			<input type="password" class="form-control" placeholder="密码" name="password"  />
			<input type="hidden"  name="remember-me" value="true" />
			<p th:if="${param.logout}" class="error-code">已成功注销</p>
			<p th:if="${param.error}" class="error-code">用户名或者密码错误</p>
			<button class="btn btn-lg btn-primary btn-block" type="submit">登录</button>
		</form>
	</div>
</body>
</html>

loggin.css

body {
  padding-top: 150px;
  padding-bottom: 40px;
  background-color: #eee;
}

.form-signin {
  max-width: 330px;
  padding: 15px;
  margin: 0 auto;
}
.form-signin .form-signin-heading{
  margin-bottom: 10px;
}

.form-signin .form-control {
  position: relative;
  font-size: 16px;
  height: auto;
  padding: 10px;
  -webkit-box-sizing: border-box;
     -moz-box-sizing: border-box;
          box-sizing: border-box;
}
.form-signin .form-control:focus {
  z-index: 2;
}
.form-signin input[type="text"] {
  margin-bottom: 15px;
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}
.form-signin input[type="password"] {
  margin-bottom: 10px;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}
.error-code{
	color: red;
	margin: 5px 0;
}

main页面随便写,完成之后就可以使用spring security了,多写几遍就能完成各种功能了

最大的难点是oauth2的配置,基本如下,自行扩展,代码很好懂的

添加两个依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.security.oauth.boot</groupId>
        <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    </dependency>
    <dependency>
        <groupId>com.nimbusds</groupId>
        <artifactId>nimbus-jose-jwt</artifactId>
    </dependency>
</dependencies>

代码如下

import java.security.KeyPair;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.AccessTokenConverter;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.UserAuthenticationConverter;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

@EnableAuthorizationServer
@Configuration
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
	AuthenticationManager authenticationManager;

	KeyPair keyPair;

	boolean jwtEnabled;

	DataSource dataSource;

	public AuthorizationServerConfiguration(AuthenticationConfiguration authenticationConfiguration, KeyPair keyPair,
			DataSource dataSource, @Value("${security.oauth2.authorizationserver.jwt.enabled:true}") boolean jwtEnabled)
			throws Exception {
		this.authenticationManager = authenticationConfiguration.getAuthenticationManager();
		this.keyPair = keyPair;
		this.jwtEnabled = jwtEnabled;
		this.dataSource = dataSource;
	}

	@Bean
	public JdbcClientDetailsService jdbcClientDetailsService() {
		return new JdbcClientDetailsService(dataSource);
	}

	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
		clients.withClientDetails(jdbcClientDetailsService());
	}

	public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
		endpoints.authenticationManager(this.authenticationManager).tokenStore(tokenStore()).reuseRefreshTokens(false);
//				.allowedTokenEndpointRequestMethods(new HttpMethod[] { HttpMethod.GET, HttpMethod.POST });
		if (this.jwtEnabled)
			endpoints.accessTokenConverter((AccessTokenConverter) accessTokenConverter());
	}

	@Bean
	public TokenStore tokenStore() {
		if (this.jwtEnabled)
			return new JdbcTokenStore(dataSource);
		return new InMemoryTokenStore();
	}

	@Bean
	public JwtAccessTokenConverter accessTokenConverter() {
		JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
		converter.setKeyPair(this.keyPair);
		DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
		accessTokenConverter
				.setUserTokenConverter((UserAuthenticationConverter) new SubjectAttributeUserTokenConverter());
		converter.setAccessTokenConverter((AccessTokenConverter) accessTokenConverter);
		return converter;
	}
}

import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.endpoint.FrameworkEndpoint;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@FrameworkEndpoint
public class IntrospectEndpoint {
	TokenStore tokenStore;

	IntrospectEndpoint(TokenStore tokenStore) {
		this.tokenStore = tokenStore;
	}

	@PostMapping({ "/introspect" })
	@ResponseBody
	public Map<String, Object> introspect(@RequestParam("token") String token) {
		OAuth2AccessToken accessToken = this.tokenStore.readAccessToken(token);
		Map<String, Object> attributes = new HashMap<>();
		if (accessToken == null || accessToken.isExpired()) {
			attributes.put("active", Boolean.valueOf(false));
			return attributes;
		}
		OAuth2Authentication authentication = this.tokenStore.readAuthentication(token);
		attributes.put("active", Boolean.valueOf(true));
		attributes.put("exp", Long.valueOf(accessToken.getExpiration().getTime()));
		attributes.put("scope", accessToken.getScope().stream().collect(Collectors.joining(" ")));
		attributes.put("sub", authentication.getName());
		return attributes;
	}
}
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import java.security.KeyPair;
import java.security.interfaces.RSAPublicKey;
import java.util.Map;
import org.springframework.security.oauth2.provider.endpoint.FrameworkEndpoint;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@FrameworkEndpoint
public class JwkSetEndpoint {
	KeyPair keyPair;

	JwkSetEndpoint(KeyPair keyPair) {
		this.keyPair = keyPair;
	}

	@GetMapping({ "/.well-known/jwks.json" })
	@ResponseBody
	public Map<String, Object> getKey() {
		RSAPublicKey publicKey = (RSAPublicKey) this.keyPair.getPublic();
		RSAKey key = (new RSAKey.Builder(publicKey)).build();
		return (Map<String, Object>) (new JWKSet((JWK) key)).toJSONObject();
	}
}
import java.io.IOException;
import java.net.URL;
import java.security.KeyPair;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.UrlResource;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import org.springframework.util.ResourceUtils;

import lombok.Setter;

@Configuration
@ConfigurationProperties(prefix = "auth")
@Setter
public class KeyConfig {
	private URL jwk;
	private String password;
	private String name;

	@Bean
	KeyPair keyPair() throws IOException {
		if(jwk == null ) {
			jwk = ResourceUtils.getURL("classpath:auth.server.pfx");
			password = "123456";
			name = "auth.server";
		}
		KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new UrlResource(jwk),
				password.toCharArray());
		return keyStoreKeyFactory.getKeyPair(name);
	}
}
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
	
	public void configure(HttpSecurity http) throws Exception {
		http.requestMatchers().antMatchers("/oauth/userInfo").and().authorizeRequests().anyRequest().authenticated();
	}

	public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
		super.configure(resources);
	}
	
}
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter;

public class SubjectAttributeUserTokenConverter extends DefaultUserAuthenticationConverter {
	
	public Map<String, ?> convertUserAuthentication(Authentication authentication) {
		Map<String, Object> response = new LinkedHashMap<>();
		response.put("sub", authentication.getName());
		if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty())
			response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
		return response;
	}
	
}
import java.io.IOException;
import java.security.Principal;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.endpoint.FrameworkEndpoint;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import lombok.extern.slf4j.Slf4j;

@FrameworkEndpoint
@Slf4j
public class UserInfoEndpoint {

	@GetMapping({ "/oauth/userInfo" })
	@ResponseBody
	public Principal userInfo(OAuth2Authentication authentication) {
		return (Principal) authentication;
	}

	@GetMapping("/oauth/exit")
	public void exit(HttpServletRequest request, HttpServletResponse response) {
		new SecurityContextLogoutHandler().logout(request, null, null);
		try {
			String referer = request.getHeader("referer");
			if(referer == null) {
				referer = "/";
			}
			log.info("logoutPage: {}", referer);
			response.sendRedirect(referer);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

}
1 个赞

我到现在也不会sc。感觉好复杂。:cold_face:

github上spring-security 5.4以前的版本都有samples这个目录,打开一看,全部一清二白

还是存在问题,这并不是标准的oauth2,且不说以前的oaut2现目已经过时了。即使没有过时,你这路子也有点也,spring security 是提供了自己的一套标准结构,无论是key,还是toekn,又或者是token 增强。这相当于放弃了部分spring security , 使用自己的接口,容易导致莫名的bug。而且又出新版的oauth2了,又得慢慢学,心累。

这就是官方实例,路子野从何谈起?莫名其妙的bug?找不出来就是没有,你觉得的bug是你根本没试过或者没理解

嗷,可能是吧。因为我看见你那个 introspect 接口,自己从toeknstore 里面来获取信息。框架本来的端点是提供了 oauth/token 端口开放的。还有公钥配置,直接在tokenConverter里面配置下就可以,通过token_key,就能拿到,确被写得好复杂。

官方代码就是这么写的,introspect接口是用来登陆之后取用户数据的,删了都行,这没什么可纠结的

写的挺清楚的 感觉 ,有个问题 new SecurityContextLogoutHandler().logout(request, null, null); 直接调logout 会将tokenStore存储的token删掉吗

会,不过这个api是给客户端调用的,客户端调用logout的时候redirect到这里执行服务端的logout

ok