丰富的线上&线下活动,深入探索云世界
做任务,得社区积分和周边
最真实的开发者用云体验
让每位学生受益于普惠算力
让创作激发创新
资深技术专家手把手带教
遇见技术追梦人
技术交流,直击现场
海量开发者使用工具、手册,免费下载
极速、全面、稳定、安全的开源镜像
开发手册、白皮书、案例集等实战精华
为开发者定制的Chrome浏览器插件
本章节使用SpringBoot集成SpringSecurity开发一个LightSword接口自动化测试平台,由浅入深的讲解SpringBoot集成SpringSecurity开发技术知识。
本章节采用SpringBoot集成的主要的后端技术框架:
编程语言:java,scalaORM框架:jpaView模板引擎:velocity安全框架:springsecurity数据库:mysql
项目pom.xml添加spring-boot-starter-security依赖
这个用户名,密码是什么呢?
让我们来从SpringBoot源码寻找一下。
你搜一下输出日志,会看到下面一段输出:
2017-04-2721:39:20.321INFO94124---[ost-startStop-1]b.a.s.AuthenticationManagerConfiguration:Usingdefaultsecuritypassword:6c920ced-f1c1-4604-96f7-f0ce4e46f5d4这段日志是AuthenticationManagerConfiguration类里面的如下方法输出的:
@Overridepublicvoidconfigure(AuthenticationManagerBuilderauth)throwsException{if(auth.isConfigured()){return;}Useruser=this.securityProperties.getUser();if(user.isDefaultPassword()){logger.info(String.format("%n%nUsingdefaultsecuritypassword:%s%n",user.getPassword()));}Set
publicstaticclassUser{/***Defaultusername.*/privateStringname="user";/***Passwordforthedefaultusername.*/privateStringpassword=UUID.randomUUID().toString();/***Grantedrolesforthedefaultusername.*/privateList
当然,如果我们想简单改一下这个用户名密码,可以在application.properties配置你的用户名密码,例如
#securitysecurity.user.name=adminsecurity.user.password=admin当然这只是一个初级的配置,更复杂的配置,可以分不用角色,在控制范围上,能够拦截到方法级别的权限控制。且看下文分解。
在上面章节,我们什么都没做,就添加了spring-boot-starter-security依赖,整个应用就有了默认的认证安全机制。下面,我们来定制用户名密码。
写一个extendsWebSecurityConfigurerAdapter的配置类:
1.通过@EnableWebSecurity注解开启SpringSecurity的功能。使用@EnableGlobalMethodSecurity(prePostEnabled=true)这个注解,可以开启security的注解,我们可以在需要控制权限的方法上面使用@PreAuthorize,@PreFilter这些注解。
2.extends继承WebSecurityConfigurerAdapter类,并重写它的方法来设置一些web安全的细节。我们结合@EnableWebSecurity注解和继承WebSecurityConfigurerAdapter,来给我们的系统加上基于web的安全机制。
5.configureGlobal(AuthenticationManagerBuilderauth)方法,在内存中创建了一个用户,该用户的名称为root,密码为root,用户角色为USER。
输入用户名,密码,点击Login
成功跳转我们之前要访问的页面:
不过,我们发现,SpringBoot应用的启动日志还是打印了如下一段:
2017-04-2722:51:44.059INFO95039---[ost-startStop-1]b.a.s.AuthenticationManagerConfiguration:Usingdefaultsecuritypassword:5fadfb54-2096-4a0b-ad46-2dad3220c825但实际上,已经使用了我们定制的用户名密码了。
如果我们要配置多个用户,多个角色,可参考使用如下示例的代码:
@Overrideprotectedvoidconfigure(AuthenticationManagerBuilderauth)throwsException{auth.inMemoryAuthentication().withUser("root").password("root").roles("USER").and().withUser("admin").password("admin").roles("ADMIN","USER").and().withUser("user").password("user").roles("USER");}角色权限控制当我们的系统功能模块当需求发展到一定程度时,会不同的用户,不同角色使用我们的系统。这样就要求我们的系统可以做到,能够对不同的系统功能模块,开放给对应的拥有其访问权限的用户使用。
SpringSecurity提供了SpringEL表达式,允许我们在定义URL路径访问(@RequestMapping)的方法上面添加注解,来控制访问权限。
在标注访问权限时,根据对应的表达式返回结果,控制访问权限:
true,表示有权限fasle,表示无权限
SpringSecurity可用表达式对象的基类是SecurityExpressionRoot。
同时,我们可以看出hasRole跟hasAnyRole是一样的。hasAnyRole是调用的hasAnyAuthorityName(defaultRolePrefix,roles)。所以,我们在学习一个框架或者一门技术的时候,最准确的就是源码。通过源码,我们可以更好更深入的理解技术的本质。
SecurityExpressionRoot为我们提供的使用SpringEL表达式总结如下[1]:
比如说,在lightsword系统中,我们设置测试报告页面,只针对ADMIN权限开放,代码如下:
在方法上添加@PreAuthorize这个注解,value="hasRole('ADMIN')")是Spring-ELexpression,当表达式值为true,标识这个方法可以被调用。如果表达式值是false,标识此方法无权限访问。
{"accountNonExpired":true,"accountNonLocked":true,"authorities":[{"authority":"ROLE_ADMIN"},{"authority":"ROLE_USER"}],"credentialsNonExpired":true,"enabled":true,"username":"root"}这个Authentication对象信息其实就是User实体的信息(当然,密码没放进来)。
publicclassUserimplementsUserDetails,CredentialsContainer{privateStringpassword;privatefinalStringusername;privatefinalSet
Objectprincipal=SecurityContextHolder.getContext().getAuthentication().getPrincipal();if(principalinstanceofUserDetails){Stringusername=((UserDetails)principal).getUsername();}else{Stringusername=principal.toString();}通过调用getContext()返回的对象是SecurityContext的实例对象,该实例对象保存在ThreadLocal线程本地存储中。使用SpringSecurity框架,通常的认证机制都是返回UserDetails实例。
SpringMVC的Web开发使用Controller基本上可以完成大部分需求,但是我们还可能会用到Servlet、Filter、Listener、Interceptor等等。
在SpringBoot中添加自己的Servlet有两种方法,代码注册Servlet和注解自动注册(Filter和Listener也是如此)。
(1)代码注册通过ServletRegistrationBean、FilterRegistrationBean和ServletListenerRegistrationBean获得控制。也可以通过实现ServletContextInitializer接口直接注册。使用代码注册Servlet(就不需要@ServletComponentScan注解)
(2)在SpringBootApplication上使用@ServletComponentScan注解后,Servlet、Filter、Listener可以直接通过@WebServlet、@WebFilter、@WebListener注解自动注册。
代码如下
valprincipal=SecurityContextHolder.getContext.getAuthentication.getPrincipalif(principal.isInstanceOf[UserDetails]){username=principal.asInstanceOf[UserDetails].getUsername}else{username=principal.toString}拿到认证信息,然后把用户名放到session中:
2.给启动类加上注解@ServletComponentScan
packagecom.springboot.in.actionimportorg.springframework.boot.autoconfigure.SpringBootApplicationimportorg.springframework.boot.web.servlet.ServletComponentScan@SpringBootApplication@ServletComponentScan(basePackages=Array("com.springboot.in.action"))classAppConfig这个注解将开启扫描Servlet组件功能。那些被标注了@WebFilter,@WebServlet,@WebListener的Bean将会注册到容器中。需要注意的一点是,这个扫描动作只在当我们使用的是嵌入式Servlet容器的时候才起作用。完成Bean注册工作的类是org.springframework.boot.web.servlet.ServletComponentScanRegistrar,它实现了Spring的ImportBeanDefinitionRegistrar接口。
3.前端显示用户信息
Velocity内置了一些对象,例如:$request、$response、$session,这些对象可以在vm模版里可以直接调用。所以我们只需要使用$session取出,当初我们放进session的对应key的属性值即可。
我们在LoginFilter里面是这样放进去的:
session.setAttribute("username",username)在前端页面直接这样取出username
部署应用运行,我们看一下运行日志:
o.s.b.c.embedded.FilterRegistrationBean:Mappingfilter:'com.springboot.in.action.filter.LoginFilter'to:[/*]这表明我们定义的LoginFilter类成功注册,路径映射到/*。同时,我们在
o.s.s.web.DefaultSecurityFilterChain:Creatingfilterchain:这行日志后面,看到SpringBoot默认创建了的那些FilterChain。这些Filter如下:
我们刚才是使用@WebFilter注解一个javax.servlet.Filter的实现类来实现一个LoginFilter。
基于JavaConfig,SpringBoot同样可以使用如下的方式实现Servlet、Filter、Listener的Bean的配置:
本节我们将在我们之前的系统上,实现一个用数据库存储用户和角色,实现系统的安全认证。在权限角色上,我们简单设计两个用户角色:USER,ADMIN。
我们设计页面的权限如下:
对应的领域实体模型类如下:
用户表
packagecom.springboot.in.action.entityimportjavax.persistence.{Entity,GeneratedValue,GenerationType,Id}importscala.beans.BeanProperty/***Createdbyjackon2017/4/29.*/@EntityclassUser{@Id@GeneratedValue(strategy=GenerationType.AUTO)@BeanPropertyvarid:Integer=_@BeanPropertyvaruserName:String=_@BeanPropertyvarpassword:String=_}角色表
packagecom.springboot.in.action.entityimportjavax.persistence.{Entity,GeneratedValue,GenerationType,Id}importscala.beans.BeanProperty/***Createdbyjackon2017/4/29.*/@EntityclassRole{@Id@GeneratedValue(strategy=GenerationType.AUTO)@BeanPropertyvarid:Integer=_@BeanPropertyvarrole:String=_}用户角色关联表
packagecom.springboot.in.action.entityimportjavax.persistence.{Entity,GeneratedValue,GenerationType,Id}importscala.beans.BeanProperty/***Createdbyjackon2017/4/29.*/@EntityclassUserRole{@Id@GeneratedValue(strategy=GenerationType.AUTO)@BeanPropertyvarid:Integer=_@BeanPropertyvaruserId:Integer=_@BeanPropertyvarroleId:Integer=_}为了方便测试,我们后面会写一个用户测试数据的自动生成的Bean,用来做测试数据的自动初始化工作。
同样的,我们要写一个继承WebSecurityConfigurerAdapter的配置类:
(1)覆盖写userDetailsService方法,具体的LightSwordUserDetailService实现类,我们下面紧接着会讲。
(2)默认不拦截静态资源的urlpattern。我们也可以用下面的WebSecurity这个方式跳过静态资源的认证
packagecom.springboot.in.action.securityimportorg.springframework.context.annotation.Configurationimportorg.springframework.web.servlet.config.annotation.{ViewControllerRegistry,WebMvcConfigurerAdapter}/***Createdbyjackon2017/4/30.*/@ConfigurationclassWebMvcConfigextendsWebMvcConfigurerAdapter{/***统一注册纯RequestMapping跳转View的Controller*/overridedefaddViewControllers(registry:ViewControllerRegistry){registry.addViewController("/login").setViewName("/login")}}这里我们直接采用ViewControllerRegistry来注册一个纯路径映射的Controller方法。
login.html
我们同样使用@EnableGlobalMethodSecurity(prePostEnabled=true)这个注解,开启security的注解,这样我们可以在需要控制权限的方法上面使用@PreAuthorize,@PreFilter这些注解。
代码如下:
我们首先添加一个GlobalExceptionHandlerAdvice,使用@ControllerAdvice注解:
packagecom.springboot.in.action.adviceimportorg.springframework.web.bind.annotation.{ControllerAdvice,ExceptionHandler}importorg.springframework.web.context.request.WebRequestimportorg.springframework.web.servlet.ModelAndView/***Createdbyjackon2017/4/27.*/@ControllerAdviceclassGlobalExceptionHandlerAdvice{@ExceptionHandler(value=Array(classOf[Exception]))//表示捕捉到所有的异常,你也可以捕捉一个你自定义的异常defexception(exception:Exception,request:WebRequest):ModelAndView={valmodelAndView=newModelAndView("/error")//error页面modelAndView.addObject("errorMessage",exception.getMessage)modelAndView.addObject("stackTrace",exception.getStackTrace)modelAndView}}其中,@ExceptionHandler(value=Array(classOf[Exception])),表示捕捉到所有的异常,这里你也可以捕捉一个你自定义的异常。比如说,针对安全认证的Exception,我们可以单独定义处理。此处不再赘述。感兴趣的读者,可自行尝试。
错误统一处理页面error.html
#parse("/common/header.html")
系统异常统一处理页面
异常消息:$errorMessage
异常堆栈信息:
#foreach($ein$stackTrace)$e#end
#parse("/common/footer.html")6.测试运行为了方便测试用户权限功能,我们给数据库初始化一些测试数据进去:我们自定义LightSwordUserDetailService实现了UserDetailsService接口,从我们自己定义的数据库表里面取得用户信息来认证鉴权。
SpringSecurity提供的功能还远不止于此,更多SpringSecurity的使用可参见【参考资料】部分。