详解Springboot Oauth2 Server搭建Oauth2认证服务

时间:2021-05-19

本教程源码
https://github.com/bestaone/HiAuth

源码比较全面,教程我就只介绍关键代码了,喜欢的点个star,谢谢!

关键词

  • 微服务认证
  • Oauth2
  • 认证中心
  • springboot
  • spring-cloud-starter-oauth2
  • 集成Oauth2
  • Oauth2 客户端

介绍

这里我将介绍两个部分

  • Oauth2 server 的开发 (hi-auth-web模块)
  • Oauth2 client 的开发 (hi-mall-web模块)

效果图

himall.gif

umc.gif

LIVE DEMO

HiMall: http://hiauth.cn/himall

UMC: http://hiauth.cn/umc

Swagger2:http://hiauth.cn/hiauth/swagger-ui.html

Oauth2 server 搭建

数据库表(mysql5.6),其中只有sys_user表由我们自己控制,其他表由框架控制

CREATE TABLE `clientdetails` ( `appId` varchar(255) NOT NULL, `resourceIds` varchar(256) DEFAULT NULL, `appSecret` varchar(256) DEFAULT NULL, `scope` varchar(256) DEFAULT NULL, `grantTypes` varchar(256) DEFAULT NULL, `redirectUrl` varchar(256) DEFAULT NULL, `authorities` varchar(256) DEFAULT NULL, `access_token_validity` int(11) DEFAULT NULL, `refresh_token_validity` int(11) DEFAULT NULL, `additionalInformation` varchar(4096) DEFAULT NULL, `autoApproveScopes` varchar(256) DEFAULT NULL, PRIMARY KEY (`appId`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE `oauth_access_token` ( `token_id` varchar(256) DEFAULT NULL, `token` blob, `authentication_id` varchar(255) NOT NULL, `user_name` varchar(256) DEFAULT NULL, `client_id` varchar(256) DEFAULT NULL, `authentication` blob, `refresh_token` varchar(256) DEFAULT NULL, PRIMARY KEY (`authentication_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE `oauth_approvals` ( `userId` varchar(256) DEFAULT NULL, `clientId` varchar(256) DEFAULT NULL, `scope` varchar(256) DEFAULT NULL, `status` varchar(10) DEFAULT NULL, `expiresAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `lastModifiedAt` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00') ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE `oauth_client_details` ( `client_id` varchar(255) NOT NULL, `resource_ids` varchar(256) DEFAULT NULL, `client_secret` varchar(256) DEFAULT NULL, `scope` varchar(256) DEFAULT NULL, `authorized_grant_types` varchar(256) DEFAULT NULL, `web_server_redirect_uri` varchar(2560) DEFAULT NULL, `authorities` varchar(256) DEFAULT NULL, `access_token_validity` int(11) DEFAULT NULL, `refresh_token_validity` int(11) DEFAULT NULL, `additional_information` varchar(4096) DEFAULT NULL, `autoapprove` varchar(256) DEFAULT NULL, PRIMARY KEY (`client_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;INSERT INTO `oauth_client_details` VALUES ('client', null, '$2a$10$1N/.LvTJuYpvxDzoJ1KdvuPDdV/kDSQE9Cxm9BzB1PreyzK6gmFRe', 'ALL,AUTH,USER,GOODS,ORDER', 'authorization_code,client_credentials,password,refresh_token', 'http://localhost:8081/mall/callback,http://localhost:9080/user/webjars/springfox-swagger-ui/oauth2-redirect.html,http://localhost:9081/goods/webjars/springfox-swagger-ui/oauth2-redirect.html,http://localhost:9082/order/webjars/springfox-swagger-ui/oauth2-redirect.html,http://localhost/user/webjars/springfox-swagger-ui/oauth2-redirect.html,http://localhost/goods/webjars/springfox-swagger-ui/oauth2-redirect.html,http://localhost/order/webjars/springfox-swagger-ui/oauth2-redirect.html', 'ROLE_USER', '1800', '86400', null, 'false');CREATE TABLE `oauth_client_token` ( `token_id` varchar(256) DEFAULT NULL, `token` blob, `authentication_id` varchar(255) NOT NULL, `user_name` varchar(256) DEFAULT NULL, `client_id` varchar(256) DEFAULT NULL, PRIMARY KEY (`authentication_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE `oauth_code` ( `code` varchar(256) DEFAULT NULL, `authentication` blob) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE `oauth_refresh_token` ( `token_id` varchar(256) DEFAULT NULL, `token` blob, `authentication` blob) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE `sys_user` ( `id` bigint(20) NOT NULL, `name` varchar(20) DEFAULT NULL, `username` varchar(20) NOT NULL, `password` varchar(128) NOT NULL, `tel` varchar(20) DEFAULT NULL, `gender` varchar(10) DEFAULT NULL, `createTime` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `unique_username` (`username`), UNIQUE KEY `unique_tel` (`tel`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;INSERT INTO `sys_user` VALUES ('1', '张三', 'admin', '123456', '13712345678', 'MALE', '2018-12-03 17:57:12');INSERT INTO `sys_user` VALUES ('2', '李四', 'user', '123456', '13812345678', 'UNKNOWN', '2018-12-03 17:57:12');

pom.xml如下

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> <version>2.0.1.RELEASE</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId></dependency><dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version></dependency>

添加表sys_user的service、mapper

@Mapperpublic interface UserMapper { @Insert("INSERT INTO sys_user(id,name,username,password,tel,gender,createTime) VALUES(#{id},#{name},#{username},#{password},#{tel},#{gender},#{createTime})") void insert(User user); @Delete("DELETE FROM sys_user WHERE id = #{id}") void delete(Long id); @Update("UPDATE sys_user SET name=#{name},username=#{username},password=#{password},tel=#{tel},gender=#{gender},createTime=#{createTime} WHERE id =#{id}") int update(User user); @ResultMap("BaseResultMap") @Select("SELECT * FROM sys_user WHERE id=#{id}") User findById(Long id); @ResultMap("BaseResultMap") @Select("SELECT * FROM sys_user WHERE username=#{username}") User findByUsername(String username); @ResultMap("BaseResultMap") @Select("SELECT * FROM sys_user WHERE tel=#{tel}") User findByTel(String tel); @ResultMap("BaseResultMap") @Select("SELECT * FROM sys_user") List<User> findAll(); @ResultMap("BaseResultMap") @Select("SELECT * FROM sys_user WHERE name like #{name}") List<User> findByName(String name);}@Servicepublic class UserServiceImpl implements UserService { @Resource UserMapper mapper; @Override public User save(User user) { if(user.getId()!=null){ mapper.update(user); } else { user.setId(System.currentTimeMillis()); mapper.insert(user); } return user; } @Override public User findById(Long id) { return mapper.findById(id); } @Override public User findByUsername(String username) { return mapper.findByUsername(username); } @Override public User findByTel(String tel) { return mapper.findByTel(tel); } @Override public List<User> findAll() { return mapper.findAll(); } @Override public void delete(Long id) { mapper.delete(id); } @Override public List<User> findByName(String name) { return mapper.findByName("%" + name + "%"); }}

添加登录拦截

@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Bean public UserDetailsService simpleUserDetailsService(){ return new UserDetailsServiceImpl(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(simpleUserDetailsService()); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http.userDetailsService(userDetailsService()); http.csrf().disable(); http.formLogin() .loginPage("/signin").loginProcessingUrl("/signin/form/account").defaultSuccessUrl("/index") .and() .logout().logoutUrl("/signout").logoutSuccessUrl("/signin") .and() .authorizeRequests() .antMatchers("/signin","/signin/form/tel","/code/image","/code/mobile","/static/**").permitAll() .antMatchers("/oauth/**").permitAll() .antMatchers("/user/**").hasAnyRole("USER","ADMIN") .anyRequest().authenticated(); }}

添加登录表单signin.html

<div class="tab-pane fade in active" id="account-login"> <form th:action="@{/signin/form/account}" method="post"> <label for="username" class="sr-only">用户名</label> <input class="form-control" type="text" name="username" id="username" value="user" placeholder="账号" required> <label for="password" class="sr-only">密码</label> <input class="form-control" type="password" name="password" id="password" value="123456" placeholder="密码" required> <button class="btn btn-lg btn-primary btn-block" type="submit">登录</button> </form></div>

Oauth2 server Config

@Configuration@EnableAuthorizationServerpublic class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter { @Autowired private Environment env; @Autowired private AuthenticationManager authenticationManager; /** * 自定义授权页面 */ @Autowired private AuthorizationEndpoint authorizationEndpoint; @PostConstruct public void init() { authorizationEndpoint.setUserApprovalPage("forward:/oauth/my_approval_page"); authorizationEndpoint.setErrorPage("forward:/oauth/my_error_page"); } @Bean public DataSource dataSource() { final DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName(env.getProperty("spring.datasource.driver-class-name")); dataSource.setUrl(env.getProperty("spring.datasource.url")); dataSource.setUsername(env.getProperty("spring.datasource.username")); dataSource.setPassword(env.getProperty("spring.datasource.password")); return dataSource; } @Bean public ApprovalStore approvalStore() { return new JdbcApprovalStore(dataSource()); } @Bean protected AuthorizationCodeServices authorizationCodeServices() { return new JdbcAuthorizationCodeServices(dataSource()); } @Bean public TokenStore tokenStore() { return new JdbcTokenStore(dataSource()); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // oauth_client_details clients.jdbc(dataSource()); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { // oauth_approvals endpoints.approvalStore(approvalStore()); // oauth_code endpoints.authorizationCodeServices(authorizationCodeServices()); // oauth_access_token & oauth_refresh_token endpoints.tokenStore(tokenStore()); // 支持password grant type endpoints.authenticationManager(authenticationManager); } @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) { oauthServer.allowFormAuthenticationForClients(); }}

Oauth2 client 搭建

pom.xml

<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>com.github.scribejava</groupId> <artifactId>scribejava-apis</artifactId> <version>5.0.0</version></dependency>

DefaultApi20

public class AiwanApi extends DefaultApi20 { private String accessTokenEndpoint = "http://localhost:8080/oauth/token"; private String authorizationBaseUrl = "http://localhost:8080/oauth/authorize"; protected AiwanApi() {} private static class InstanceHolder { private static final AiwanApi INSTANCE = new AiwanApi(); } public static AiwanApi instance() { return InstanceHolder.INSTANCE; } @Override public String getAccessTokenEndpoint() { return accessTokenEndpoint; } @Override protected String getAuthorizationBaseUrl() { return authorizationBaseUrl; } @Override public TokenExtractor<OAuth2AccessToken> getAccessTokenExtractor() { return OAuth2AccessTokenJsonExtractor.instance(); } @Override public OAuth20Service createService(OAuthConfig config) { return new AiwanService(this, config); }}

OAuth20Service

public class AiwanService extends OAuth20Service { public AiwanService(DefaultApi20 api, OAuthConfig config) { super(api, config); } @Override protected OAuthRequest createAccessTokenRequest(String code) { final OAuthRequest request = new OAuthRequest(getApi().getAccessTokenVerb(), getApi().getAccessTokenEndpoint()); final OAuthConfig config = getConfig(); request.addParameter(OAuthConstants.CLIENT_ID, config.getApiKey()); final String apiSecret = config.getApiSecret(); if (apiSecret != null) { request.addParameter(OAuthConstants.CLIENT_SECRET, apiSecret); } request.addParameter(OAuthConstants.CODE, code); request.addParameter(OAuthConstants.REDIRECT_URI, config.getCallback()); final String scope = config.getScope(); if (scope != null) { request.addParameter(OAuthConstants.SCOPE, scope); } request.addParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.AUTHORIZATION_CODE); request.addHeader(OAuthConstants.HEADER, OAuthConstants.BASIC + ' ' + Base64Encoder.getInstance() .encode(String.format("%s:%s", config.getApiKey(), apiSecret).getBytes(Charset.forName("UTF-8")))); return request; }}

获取access_token

@Controllerpublic class IndexController { private static Logger logger = LoggerFactory.getLogger(IndexController.class); private static final String SESSION_KEY_ACCESS_TOKEN = "MY_ACCESS_TOKEN"; /** * 为防止CSRF跨站攻击,每次请求STATE的值应该不同,可以放入Session! * 由于都是localhost测试,所以session无法保持,用一个固定值。 */ private static final String STATE = "secret-rensanning"; private static final String CLIENT_ID = "client"; private static final String CLIENT_SECRET = "123456"; private static final String CALLBACK_URL = "http://localhost:8081/mall/callback"; private static final String SCOPE = "ALL"; private OAuth20Service aiwanApi = new ServiceBuilder(CLIENT_ID) .apiSecret(CLIENT_SECRET) .scope(SCOPE) .state(STATE) .callback(CALLBACK_URL) .build(AiwanApi.instance()); @GetMapping("/") public String index() { return "index"; } @GetMapping("/signin") public void signin(HttpServletRequest request, HttpServletResponse response) throws IOException { logger.debug("signin"); logger.info("session id:{}", request.getSession().getId()); String authorizationUrl = aiwanApi.getAuthorizationUrl(); logger.info("redirectURL:{}", authorizationUrl); response.sendRedirect(authorizationUrl); } @GetMapping("/callback") public String callback(@RequestParam(value = "code", required = false) String code, @RequestParam(value = "state", required = false) String state, HttpServletRequest request) throws Exception { logger.debug("callback [code:{}],[state:{}],[sessionId:{}]", code, state, request.getSession().getId()); if (STATE.equals(state)) { logger.info("State OK!"); } else { logger.error("State NG!"); } OAuth2AccessToken accessToken = aiwanApi.getAccessToken(code); request.getSession().setAttribute(SESSION_KEY_ACCESS_TOKEN, accessToken); return "profile"; }}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

声明:本页内容来源网络,仅供用户参考;我单位不保证亦不表示资料全面及准确无误,也不保证亦不表示这些资料为最新信息,如因任何原因,本网内容或者用户因倚赖本网内容造成任何损失或损害,我单位将不会负任何法律责任。如涉及版权问题,请提交至online#300.cn邮箱联系删除。

相关文章