这里主要介绍spring-cloud的难点问题,spring-security,以及吐槽
spring-security包含oauth2的部分是整个spring-cloud的难点
本人常年混迹外包,流浪n个公司,可无一个公司能做好登陆系统,野路子横行,代码杂乱霸道,领导得过且过,证明外包都是垃圾的话是可信的,我也无力反驳,但其实做一个登陆系统并非什么难事
oauth2并不困难,下面是个简单的例子
首先介绍关于密钥的几个命令:
openssl在linux机上执行,keytool在win机上执行,当然了先生成jre并加入
环境变量是必要的。
- 私钥:
openssl genrsa -out auth.server.key 4096
- 公钥:
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
- 生成p12文件,后缀可以是p12,pfx等,密码都用123456
openssl pkcs12 -export -in auth.server.crt -inkey auth.server.key -out auth.server.p12 -name "auth.server"
- 导出信任证书
keytool -export -alias auth.server -auth.server.p12 -storepass 123456 -file auth.server.cer
- 信任证书导入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();
}
}
}