基于Spring Security的Oauth2授权实现方法
前言
经过一段时间的学习oauth2,在网上也借鉴学习了一些大牛的经验,推荐在学习的过程中多看几遍阮一峰的《理解oauth 2.0》,经过对oauth2的多种方式的实现,个人推荐spring security和oauth2的实现是相对优雅的,理由如下:
1、相对于直接实现oauth2,减少了很多代码量,也就减少的查找问题的成本。
2、通过调整配置文件,灵活配置oauth相关配置。
3、通过结合路由组件(如zuul),更好的实现微服务权限控制扩展。
oauth2概述
oauth2根据使用场景不同,分成了4种模式
- 授权码模式(authorization code)
- 简化模式(implicit)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
在项目中我们通常使用授权码模式,也是四种模式中最复杂的,通常网站中经常出现的微博,qq第三方登录,都会采用这个形式。
oauth2授权主要由两部分组成:
- authorization server:认证服务
- resource server:资源服务
在实际项目中以上两个服务可以在一个服务器上,也可以分开部署。
准备阶段
核心maven依赖如下
<dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-web</artifactid> </dependency> <dependency> <groupid>com.fasterxml.jackson.datatype</groupid> <artifactid>jackson-datatype-joda</artifactid> </dependency> <dependency> <groupid>org.thymeleaf.extras</groupid> <artifactid>thymeleaf-extras-springsecurity4</artifactid> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-thymeleaf</artifactid> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-security</artifactid> </dependency> <dependency> <groupid>org.springframework.security.oauth</groupid> <artifactid>spring-security-oauth2</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.springframework.boot</groupid> <artifactid>spring-boot-starter-data-jpa</artifactid> </dependency>
token的存储主流有三种方式,分别为内存、redis和数据库,在实际项目中通常使用redis和数据库存储。个人推荐使用mysql数据库存储。
初始化数据结构、索引和数据sql语句如下:
-- -- oauth sql -- mysql -- drop table if exists oauth_client_details; create table oauth_client_details ( client_id varchar(255) primary key, resource_ids varchar(255), client_secret varchar(255), scope varchar(255), authorized_grant_types varchar(255), web_server_redirect_uri varchar(255), authorities varchar(255), access_token_validity integer, refresh_token_validity integer, additional_information text, autoapprove varchar (255) default 'false' ) engine=innodb default charset=utf8; drop table if exists oauth_access_token; create table oauth_access_token ( token_id varchar(255), token blob, authentication_id varchar(255), user_name varchar(255), client_id varchar(255), authentication blob, refresh_token varchar(255) ) engine=innodb default charset=utf8; drop table if exists oauth_refresh_token; create table oauth_refresh_token ( token_id varchar(255), token blob, authentication blob ) engine=innodb default charset=utf8; drop table if exists oauth_code; create table oauth_code ( code varchar(255), authentication blob ) engine=innodb default charset=utf8; -- add indexes create index token_id_index on oauth_access_token (token_id); create index authentication_id_index on oauth_access_token (authentication_id); create index user_name_index on oauth_access_token (user_name); create index client_id_index on oauth_access_token (client_id); create index refresh_token_index on oauth_access_token (refresh_token); create index token_id_index on oauth_refresh_token (token_id); create index code_index on oauth_code (code); -- insert default data insert into `oauth_client_details` values ('dev', '', 'dev', 'app', 'authorization_code', 'http://localhost:7777/', '', '3600', '3600', '{"country":"cn","country_code":"086"}', 'taiji');
核心配置
核心配置主要分为授权应用和客户端应用两部分,如下:
- 授权应用:即oauth2授权服务,主要包括spring security、认证服务和资源服务两部分配置
- 客户端应用:即通过授权应用进行认证的应用,多个客户端应用间支持单点登录
授权应用主要配置如下:
application.properties链接已初始化oauth2的数据库即可
application启动类,授权服务开启配置和spring security配置,如下:
@springbootapplication @autoconfigureafter(jacksonautoconfiguration.class) @order(securityproperties.access_override_order) @enableauthorizationserver public class application extends websecurityconfigureradapter { public static void main(string[] args) { springapplication.run(application.class, args); } // 启动的时候要注意,由于我们在controller中注入了resttemplate,所以启动的时候需要实例化该类的一个实例 @autowired private resttemplatebuilder builder; // 使用resttemplatebuilder来实例化resttemplate对象,spring默认已经注入了resttemplatebuilder实例 @bean public resttemplate resttemplate() { return builder.build(); } @configuration public class webmvcconfig extends webmvcconfigureradapter { @override public void addviewcontrollers(viewcontrollerregistry registry) { registry.addviewcontroller("/login").setviewname("login"); } } @override protected void configure(httpsecurity http) throws exception { http.headers().frameoptions().disable(); http.authorizerequests() .antmatchers("/403").permitall() // for test .antmatchers("/login", "/oauth/authorize", "/oauth/confirm_access", "/appmanager").permitall() // for login .antmatchers("/image", "/js/**", "/fonts/**").permitall() // for login .antmatchers("/j_spring_security_check").permitall() .antmatchers("/oauth/authorize").authenticated(); /*.anyrequest().fullyauthenticated();*/ http.formlogin().loginpage("/login").failureurl("/login?error").permitall() .and() .authorizerequests().anyrequest().authenticated() .and().logout().invalidatehttpsession(true) .and().sessionmanagement().maximumsessions(1).expiredurl("/login?expired").sessionregistry(sessionregistry()); http.csrf().csrftokenrepository(cookiecsrftokenrepository.withhttponlyfalse()); http.rememberme().disable(); http.httpbasic(); } }
资源服务开启,如下:
@configuration @enableresourceserver protected static class resourceserverconfiguration extends resourceserverconfigureradapter { @override public void configure(httpsecurity http) throws exception { http.antmatcher("/me").authorizerequests().anyrequest().authenticated(); } }
oauth2认证授权服务配置,如下:
@configuration public class authorizationserverconfiguration extends authorizationserverconfigureradapter { public static final logger logger = loggerfactory.getlogger(authorizationserverconfiguration.class); @autowired private authenticationmanager authenticationmanager; @autowired private datasource datasource; @bean public tokenstore tokenstore() { return new jdbctokenstore(datasource); } @override public void configure(authorizationserverendpointsconfigurer endpoints) throws exception { endpoints.authenticationmanager(authenticationmanager); endpoints.tokenstore(tokenstore()); // 配置tokenservices参数 defaulttokenservices tokenservices = new defaulttokenservices(); tokenservices.settokenstore(endpoints.gettokenstore()); tokenservices.setsupportrefreshtoken(false); tokenservices.setclientdetailsservice(endpoints.getclientdetailsservice()); tokenservices.settokenenhancer(endpoints.gettokenenhancer()); tokenservices.setaccesstokenvalidityseconds( (int) timeunit.minutes.toseconds(10)); //分钟 endpoints.tokenservices(tokenservices); } @override public void configure(authorizationserversecurityconfigurer oauthserver) throws exception { oauthserver.checktokenaccess("isauthenticated()"); oauthserver.allowformauthenticationforclients(); } @bean public clientdetailsservice clientdetails() { return new jdbcclientdetailsservice(datasource); } @override public void configure(clientdetailsserviceconfigurer clients) throws exception { clients.withclientdetails(clientdetails()); /* * 基于内存配置项 * clients.inmemory() .withclient("community") .secret("community") .authorizedgranttypes("authorization_code").redirecturis("http://tech.taiji.com.cn/") .scopes("app").and() .withclient("dev") .secret("dev") .authorizedgranttypes("authorization_code").redirecturis("http://localhost:7777/") .scopes("app");*/ } }
客户端应用主要配置如下:
application.properties中oauth2配置,如下
security.oauth2.client.clientid=dev security.oauth2.client.clientsecret=dev security.oauth2.client.accesstokenuri=http://localhost:9999/oauth/token security.oauth2.client.userauthorizationuri=http://localhost:9999/oauth/authorize security.oauth2.resource.loadbalanced=true security.oauth2.resource.userinfouri=http://localhost:9999/me security.oauth2.resource.logout.url=http://localhost:9999/revoke-token security.oauth2.default.rolename=role_user
oauth2config配置,授权oauth2sso配置和spring security配置,如下:
@configuration @enableoauth2sso public class oauth2config extends websecurityconfigureradapter{ @autowired customssologouthandler customssologouthandler; @autowired oauth2clientcontext oauth2clientcontext; @bean public httpfirewall allowurlencodedslashhttpfirewall() { stricthttpfirewall firewall = new stricthttpfirewall(); firewall.setallowurlencodedslash(true); firewall.setallowsemicolon(true); return firewall; } @bean @configurationproperties("security.oauth2.client") public authorizationcoderesourcedetails taiji() { return new authorizationcoderesourcedetails(); } @bean public communitysuccesshandler customsuccesshandler() { communitysuccesshandler customsuccesshandler = new communitysuccesshandler(); customsuccesshandler.setdefaulttargeturl("/"); return customsuccesshandler; } @bean public customfailurehandler customfailurehandler() { customfailurehandler customfailurehandler = new customfailurehandler(); customfailurehandler.setdefaultfailureurl("/index"); return customfailurehandler; } @bean @primary @configurationproperties("security.oauth2.resource") public resourceserverproperties taijioauthorresource() { return new resourceserverproperties(); } @bean @override public authenticationmanager authenticationmanagerbean() throws exception { list<authenticationprovider> authenticationproviderlist = new arraylist<authenticationprovider>(); authenticationproviderlist.add(customauthenticationprovider()); authenticationmanager authenticationmanager = new providermanager(authenticationproviderlist); return authenticationmanager; } @autowired public taijiuserdetailserviceimpl userdetailsservice; @bean public taijiauthenticationprovider customauthenticationprovider() { taijiauthenticationprovider customauthenticationprovider = new taijiauthenticationprovider(); customauthenticationprovider.setuserdetailsservice(userdetailsservice); return customauthenticationprovider; } @autowired private menuservice menuservice; @autowired private roleservice roleservice; @bean public taijisecuritymetadatasource taijisecuritymetadatasource() { taijisecuritymetadatasource fismetadatasource = new taijisecuritymetadatasource(); // fismetadatasource.setmenuservice(menuservice); fismetadatasource.setroleservice(roleservice); return fismetadatasource; } @autowired private communityaccessdecisionmanager accessdecisionmanager; @bean public communityfiltersecurityinterceptor communityfiltersecurityinterceptor() throws exception { communityfiltersecurityinterceptor taijifiltersecurityinterceptor = new communityfiltersecurityinterceptor(); taijifiltersecurityinterceptor.setfismetadatasource(taijisecuritymetadatasource()); taijifiltersecurityinterceptor.setaccessdecisionmanager(accessdecisionmanager); taijifiltersecurityinterceptor.setauthenticationmanager(authenticationmanagerbean()); return taijifiltersecurityinterceptor; } @override protected void configure(httpsecurity http) throws exception { http.authorizerequests() // .antmatchers("/").permitall() // .antmatchers("/login").permitall() // // .antmatchers("/image").permitall() // // .antmatchers("/upload/*").permitall() // for // .antmatchers("/common/**").permitall() // for // .antmatchers("/community/**").permitall() // .antmatchers("/").anonymous() .antmatchers("/personal/**").authenticated() .antmatchers("/notify/**").authenticated() .antmatchers("/admin/**").authenticated() .antmatchers("/manage/**").authenticated() .antmatchers("/**/personal/**").authenticated() .antmatchers("/user/**").authenticated() .anyrequest() .permitall() // .authenticated() .and() .logout() .logoutrequestmatcher(new antpathrequestmatcher("/logout")) .addlogouthandler(customssologouthandler) .deletecookies("jsessionid").invalidatehttpsession(true) .and() .csrf().disable() //.csrftokenrepository(cookiecsrftokenrepository.withhttponlyfalse()) //.and() .addfilterbefore(loginfilter(), basicauthenticationfilter.class) .addfilterafter(communityfiltersecurityinterceptor(), filtersecurityinterceptor.class);///taijisecurity权限控制 } @override public void configure(websecurity web) throws exception { // 解决静态资源被拦截的问题 web.ignoring().antmatchers("/theme/**") .antmatchers("/community/**") .antmatchers("/common/**") .antmatchers("/upload/*"); web.httpfirewall(allowurlencodedslashhttpfirewall()); } public oauth2clientauthenticationprocessingfilter loginfilter() throws exception { oauth2clientauthenticationprocessingfilter ff = new oauth2clientauthenticationprocessingfilter("/login"); oauth2resttemplate resttemplate = new oauth2resttemplate(taiji(),oauth2clientcontext); ff.setresttemplate(resttemplate); userinfotokenservices tokenservices = new userinfotokenservices(taijioauthorresource().getuserinfouri(), taiji().getclientid()); tokenservices.setresttemplate(resttemplate); ff.settokenservices(tokenservices); ff.setauthenticationsuccesshandler(customsuccesshandler()); ff.setauthenticationfailurehandler(customfailurehandler()); return ff; } }
授权成功回调类,认证成功用户落地,如下:
public class communitysuccesshandler extends savedrequestawareauthenticationsuccesshandler { protected final log logger = logfactory.getlog(this.getclass()); private requestcache requestcache = new httpsessionrequestcache(); @autowired private userservice userservice; @autowired private roleservice roleservice; @inject authenticationmanager authenticationmanager; @value("${security.oauth2.default.rolename}") private string defaultrole; @inject taijioperationlogservice taijioperationlogservice; @inject communityconfiguration communityconfiguration; @inject private objectmapper objectmapper; @scorerule(code="login_score") @override public void onauthenticationsuccess(httpservletrequest request, httpservletresponse response, authentication authentication) throws servletexception, ioexception { // 存放authentication到securitycontextholder securitycontextholder.getcontext().setauthentication(authentication); httpsession session = request.getsession(true); // 在session中存放security context,方便同一个session中控制用户的其他操作 session.setattribute("spring_security_context", securitycontextholder.getcontext()); oauth2authentication oauth2authentication = (oauth2authentication) authentication; object details = oauth2authentication.getuserauthentication().getdetails(); userdto user = saveuser((map) details);//用户落地 collection<grantedauthority> obtionedgrantedauthorities = obtiongrantedauthorities(user); usernamepasswordauthenticationtoken newtoken = new usernamepasswordauthenticationtoken( new user(user.getloginname(), "", true, true, true, true, obtionedgrantedauthorities), authentication.getcredentials(), obtionedgrantedauthorities); newtoken.setdetails(details); object oath2details=oauth2authentication.getdetails(); oauth2authentication = new oauth2authentication(oauth2authentication.getoauth2request(), newtoken); oauth2authentication.setdetails(oath2details); oauth2authentication.setauthenticated(true); securitycontextholder.getcontext().setauthentication(oauth2authentication); logutil.log2database(taijioperationlogservice, request, user.getloginname(), "user", "", "", "user_login", "登录", "onauthenticationsuccess",""); session.setattribute("user", user); collection<grantedauthority> authorities = (collection<grantedauthority>) authentication.getauthorities(); savedrequest savedrequest = requestcache.getrequest(request, response); if (savedrequest == null) { super.onauthenticationsuccess(request, response, authentication); return; } string targeturlparameter = gettargeturlparameter(); if (isalwaysusedefaulttargeturl() || (targeturlparameter != null && stringutils.hastext(request.getparameter(targeturlparameter)))) { requestcache.removerequest(request, response); super.onauthenticationsuccess(request, response, authentication); return; } clearauthenticationattributes(request); // use the defaultsavedrequest url string targeturl = savedrequest.getredirecturl(); // logger.debug("redirecting to defaultsavedrequest url: " + targeturl); logger.debug("redirecting to last savedrequest url: " + targeturl); getredirectstrategy().sendredirect(request, response, targeturl); // getredirectstrategy().sendredirect(request, response, this.getdefaulttargeturl()); } public void setrequestcache(requestcache requestcache) { this.requestcache = requestcache; } //用户落地 private userdto saveuser(map userinfo) { userdto dto=null; try { string json = objectmapper.writevalueasstring(userinfo); dto = objectmapper.readvalue(json,userdto.class); } catch (jsonprocessingexception e) { // todo auto-generated catch block e.printstacktrace(); } catch (ioexception e) { // todo auto-generated catch block e.printstacktrace(); } userdto user=userservice.findbyloginname(dto.getloginname()); if(user!=null) { return user; } set<roledto> roles= new hashset<roledto>(); roledto role = roleservice.findbyrolename(defaultrole); roles.add(role); dto.setroles(roles); list<userdto> list = new arraylist<userdto>(); list.add(dto); dto.generatetokenforcommunity(communityconfiguration.getcontrollersalt()); string id =userservice.saveuserwithrole(dto,communityconfiguration.getcontrollersalt()); dto.setid(id); return dto; } /** * map转成实体对象 * * @param map map实体对象包含属性 * @param clazz 实体对象类型 * @return */ public static <t> t map2object(map<string, object> map, class<t> clazz) { if (map == null) { return null; } t obj = null; try { obj = clazz.newinstance(); field[] fields = obj.getclass().getdeclaredfields(); for (field field : fields) { int mod = field.getmodifiers(); if (modifier.isstatic(mod) || modifier.isfinal(mod)) { continue; } field.setaccessible(true); string filedtypename = field.gettype().getname(); if (filedtypename.equalsignorecase("java.util.date")) { string datetimestamp = string.valueof(map.get(field.getname())); if (datetimestamp.equalsignorecase("null")) { field.set(obj, null); } else { field.set(obj, new date(long.parselong(datetimestamp))); } } else { string v = map.get(field.getname()).tostring(); field.set(obj, map.get(field.getname())); } } } catch (exception e) { e.printstacktrace(); } return obj; } // 取得用户的权限 private collection<grantedauthority> obtiongrantedauthorities(userdto users) { collection<grantedauthority> authset = new hashset<grantedauthority>(); // 获取用户角色 set<roledto> roles = users.getroles(); if (null != roles && !roles.isempty()) for (roledto role : roles) { authset.add(new simplegrantedauthority(role.getid())); } return authset; } }
客户端应用,单点登录方法,如下:
@requestmapping(value = "/loadtoken", method = { requestmethod.get }) public void loadtoken(model model,httpservletresponse response,@requestparam(value = "clientid", required = false) string clientid) { string token = ""; requestattributes ra = requestcontextholder.getrequestattributes(); servletrequestattributes sra = (servletrequestattributes) ra; httpservletrequest request = sra.getrequest(); httpsession session = request.getsession(); if (session.getattribute("spring_security_context") != null) { securitycontext securitycontext = (securitycontext)session.getattribute("spring_security_context"); authentication authentication = securitycontext.getauthentication(); oauth2authenticationdetails oauth2authenticationdetails = (oauth2authenticationdetails) authentication.getdetails(); token = oauth2authenticationdetails.gettokenvalue(); } try { string url = "http://localhost:9999/rediect?clientid=dev&token="+token; response.sendredirect(url); } catch (ioexception e) { e.printstacktrace(); } }
服务端应用,单点登录方法,如下:
@requestmapping("/rediect") public string rediect(httpservletresponse responsel, string clientid, string token) { oauth2authentication authentication = tokenstore.readauthentication(token); if (authentication == null) { throw new invalidtokenexception("invalid access token: " + token); } oauth2request request = authentication.getoauth2request(); map map = new hashmap(); map.put("code", request.getrequestparameters().get("code")); map.put("grant_type", request.getrequestparameters().get("grant_type")); map.put("response_type", request.getrequestparameters().get("response_type")); //todo 需要查询一下要跳转的client_id配置的回调地址 map.put("redirect_uri", "http://127.0.0.1:8888"); map.put("client_id", clientid); map.put("state", request.getrequestparameters().get("state")); request = new oauth2request(map, clientid, request.getauthorities(), request.isapproved(), request.getscope(), request.getresourceids(), map.get("redirect_uri").tostring(), request.getresponsetypes(),request.getextensions()); // 模拟用户登录 authentication t = tokenstore.readauthentication(token); oauth2authentication auth = new oauth2authentication(request, t); oauth2accesstoken new_token = defaulttokenservices.createaccesstoken(auth); return "redirect:/user_info?access_token=" + new_token.getvalue(); } @requestmapping({ "/user_info" }) public void user(string access_token,httpservletresponse response) { oauth2authentication auth=tokenstore.readauthentication(access_token); oauth2request request=auth.getoauth2request(); map<string, string> map = new linkedhashmap<>(); map.put("loginname", auth.getuserauthentication().getname()); map.put("password", auth.getuserauthentication().getname()); map.put("id", auth.getuserauthentication().getname()); try { response.sendredirect(request.getredirecturi()+"?name="+auth.getuserauthentication().getname()); } catch (ioexception e) { e.printstacktrace(); } }
个人总结
oauth2的设计相对复杂,需要深入学习多看源码才能了解内部的一些规则,如数据token的存储是用的实体序列化后内容,需要反序列才能在项目是使用,也许是为了安全,但在学习过程需要提前掌握,还有在token的过期时间不能为0,通常来讲过期时间为0代表长期有效,但在oauth2中则报错,这些坑需要一点点探索。
通过集成spring security和oauth2较大的提供的开发的效率,也提供的代码的灵活性和可用性。但封装的核心类需要大家都了解一下,通读下代码,以便在项目中可随时获取需要的参数。
示例代码
以下是个人的一套代码,供参考。
基于spring cloud的微服务框架集成oauth2的代码示例
oauth2数据结构,如下:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
推荐阅读
-
Spring Boot Security OAuth2 实现支持JWT令牌的授权服务器
-
基于Nacos实现Spring Cloud Gateway实现动态路由的方法
-
基于Docker的MongoDB实现授权访问的方法
-
基于Spring Security的Oauth2授权实现方法
-
spring-security实现的token授权
-
Spring Security——认证授权的概念、授权的数据模型、RBAC实现授权
-
Spring Cloud下基于OAUTH2认证授权的实现示例
-
Spring Boot Security OAuth2 实现支持JWT令牌的授权服务器
-
基于Docker的MongoDB实现授权访问的方法
-
Spring Cloud实战 | 最终篇:Spring Cloud Gateway+Spring Security OAuth2集成统一认证授权平台下实现注销使JWT失效方案