安全对于任何项目来说都是非常重要的,一般而言任何项目都会有较为严格的认证和授权操作,在Java开发领域常见的安全框架有Shiro和Spring Security。Shiro是一个轻量级的安全管理框架,提供了认证、授权、会话管理、密码管理、缓存管理等功能,而Spring Security则是一个更为复杂的安全管理框架,功能比Shiro更强大,权限控制细粒度更高,对于OAuth2的支持也更友好,再加上Spring Security源自Spring全家桶,因此与Spring框架可以做到无缝整合,尤其是SpringBoot中提供的自动化配置方案,可以让Spring Security使用变得更加便捷。本篇学习Spring Security在SpringBoot框架中的使用。
Spring Security基本配置 SpringBoot针对Spring Security提供了自动化的配置方案,因为SpringBoot整合Spring Security非常简单,这也是SpringBoot项目中使用Spring Security的优势所在。接下来介绍SpringBoot中如何整合Spring Security安全框架。
基本用法 第一步,新建一个项目,并添加依赖。 使用spring Initializr
构建工具构建一个SpringBoot的Web应用,名称为springsecurityspringboot
,然后在pom.xml文件中添加如下依赖:
1 2 3 4 5 6 7 8 9 <!--添加springSecurity依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
请注意只要开发者在项目中添加了spring-boot-starter-security
依赖,那么项目中所有资源都会被保护起来。第二步,新建测试Controller类。 新建一个controller测试类HelloController,里面的代码为:
1 2 3 4 5 6 7 @RestController public class HelloController { @GetMapping("/hello") public String hello(){ return "hello,envy!"; } }
第三步,启动项目进行测试。 接下来启动项目,启动成功后访问http://localhost:8080/hello
链接时,可以看到该链接会自动跳转到登录页面,这个登录页面是由SpringSecurity提供的,如下图所示:
请注意默认的用户名是user,默认的登录密码则在每次启动项目时随机生成,查看项目启动日志就能发现诸如如下图片所示的密码:
从项目启动的日志中获取的密码和user用户名就能进行登录,登录成功后用户就能访问http://localhost:8080/hello
链接,并显示正确的信息。
配置用户名和密码 开发者如果需要修改默认的用户名和密码,可以在application.properties
配置文件中添加如下配置来达到修改默认用户名、密码以及用户角色的目的:
1 2 3 spring.security.user.name=envy spring.security.user.password=1234 spring.security.user.roles=admin
之后再重新启动项目,此时项目启动日志中就不会打印出随机生成的密码,用户可以使用自定义的用户名和密码进行登录,登录成功后,用户还具有另一个角色:admin,因为开发者在配置文件中给予该用于admin角色了。
基于内存的认证 当然开发者也可以自定义类,然后继承WebSecurityConfigurerAdapter
进而实现对SpringSecurity更多的自定义配置。如基于内存的认证,则其对应的配置方式为:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Configuration public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder passwordEncoder(){ return NoOpPasswordEncoder.getInstance(); } protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("admin").password("1234").roles("ADMIN","USER") .and().withUser("envy").password("1234").roles("USER"); } }
简单解释一下上述代码的含义:
首先自定义MyWebSecurityConfig
类继承自WebSecurityConfigurerAdapter
,且重写了configure(AuthenticationManagerBuilder auth)
方法,在该方法中配置了两个用户,一个用户名是admin,密码是1234,具备两个角色ADMIN和USER;而另一个用户名是envy,密码是1234,仅仅具备一个角色USER。
本例子仅仅只是用于demo演示,使用的SpringSecurity版本是5.1.15,在SpringSecurity5.x中引入了多种密码加密方式,开发者必须制定一种,请注意本例子使用的NoOpPasswordEncoder
已经过时,所以这里也只是demo,实际工作中不能使用该NoOpPasswordEncoder
方式,且这种是不对密码进行加密的方式,非常不安全。基于内存的用户配置角色时,不用在前面添加ROLE_
前缀,这一点需要和后续介绍的基于数据库的认证有较大差别。
配置完成后,重启项目,接着就可以使用这两个配置用户进行登录了。
HttpSecurity 虽然现在可以实现认证功能,但是受保护的资源都是默认的,且不能根据实际情况进行角色管理,如果想要实现这些功能,就需要重写WebSecurityConfigurerAdapter
中的另一个configure方法,相应的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Configuration public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder passwordEncoder(){ return NoOpPasswordEncoder.getInstance(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("root").password("1234").roles("ADMIN","DBA") .and() .withUser("admin").password("1234").roles("ADMIN","USER") .and() .withUser("envy").password("1234").roles("USER"); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.authorizeRequests() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/user/**").access("hasAnyRole('ADMIN','USER')") .antMatchers("/db/**").access("hasRole('ADMIN')and hasRole('DBA')") .anyRequest() .authenticated() .and() .formLogin() .loginProcessingUrl("/login") .permitAll() .and() .csrf() .disable(); }
简单解释一下上述代码的含义:
首先配置了三个用户,其中root用户具备ADMIN和DBA的角色,admin用户具备ADMIN和USER的角色,而envy用户仅仅是具备USER角色。
httpSecurity.authorizeRequests()
方法开启HttpSecurity的配置,然后分别配置用户访问/admin/**
模式的URL必须具备ADMIN的角色;访问/user/**
模式的URL必须具备ADMIN或者USER的角色;访问/db/**
模式的URL必须同时具备ADMIN和DBA的角色。
.anyRequest().authenticated()
表示除了前面定义的URL模式外,用户访问其他的URL都必须认证后才能访问(即登录后才能访问)。
.and().formLogin().loginProcessingUrl("/login").permitAll()
表示开启表单登录,也就是一开始看到的登录页面,同时配置了登录接口为/login
,即可以直接调用/login
接口,发起一个POST请求进行登录,登录参数中用户名必须命名为username,密码必须命名为password,配置loginProcessingUrl
接口主要是方便Ajax或者移动端调用登录接口。最后还配置了permitAll
表示和登录相关的接口都不需要认证即可访问。
.and().csrf().disable();
表示关闭csrf验证。
配置完成后,接下来在HelloController类中添加如下接口开始进行测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @RestController public class HelloController { @GetMapping("/admin/hello") public String admin(){ return "hello,admin!"; } @GetMapping("/user/hello") public String user(){ return "hello,user!"; } @GetMapping("/db/hello") public String dba(){ return "hello,dba!"; } @GetMapping("/hello") public String hello(){ return "hello!"; } }
根据前面在MyWebSecurityConfig
类中的配置可知,/admin/hello
接口只有root和admin用户具有访问权限;/user/hello
接口只有admin和envy用户具有访问权限;/db/hello
接口只有root用户具有访问权限;而/hello
接口只要是登录用户都是可以访问的。接下来就可以运行项目,在浏览器中对上述分析结果进行验证即可,这里就不分析了。
登录表单详细配置 迄今为止开发者使用的登录表单都是SpringSecurity提供的默认页面,且登录成功后的页面跳转也是默认的,但是在实际开发过程中,尤其是在前后端分离的架构中,前后端数据是通过JSON来交互的,这时登录成功后就不是页面跳转,而是一段JSON提示。如果开发者想实现这些功能,只需要完善上文的配置即可。首先继续完善configure(HttpSecurity httpSecurity)
方法中的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.authorizeRequests() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/user/**").access("hasAnyRole('ADMIN','USER')") .antMatchers("/db/**").access("hasRole('ADMIN')and hasRole('DBA')") .anyRequest() .authenticated() .and() .formLogin() .loginPage("/login_page") .loginProcessingUrl("/login") .usernameParameter("username") .passwordParameter("password") .successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { Object principal =authentication.getPrincipal(); httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); httpServletResponse.setStatus(200); Map<String,Object> map =new HashMap<>(); map.put("status",200); map.put("msg",principal); ObjectMapper objectMapper = new ObjectMapper(); out.write(objectMapper.writeValueAsString(map)); out.flush(); out.close(); } }) .failureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); httpServletResponse.setStatus(401); Map<String,Object> map =new HashMap<>(); map.put("status",401); if(e instanceof LockedException){ map.put("msg","账户被锁定,登录失败!"); }else if(e instanceof BadCredentialsException){ map.put("msg","账户名或密码输入错误,登录失败!"); }else if(e instanceof DisabledException){ map.put("msg","账户被禁用,登录失败!"); }else if(e instanceof AccountExpiredException){ map.put("msg","账户已过期,登录失败!"); }else{ map.put("msg","登录失败!"); } ObjectMapper objectMapper = new ObjectMapper(); out.write(objectMapper.writeValueAsString(map)); out.flush(); out.close(); } }); }
简单解释一下上述代码的含义:
修改了configure(HttpSecurity httpSecurity)
方法,首先配置了loginPage也就是登录页面,配置了loginPage后,如果用户未授权就访问一个需要授权才能访问的接口,那么系统就会自动跳转到login_page页面让用户登录,而这个login_page就是开发者自定义的登录页面,而不再是SpringSecurity提供的默认登录页面。
然后配置了loginProcessingUrl
表示登录请求处理接口,不论是自定义登录页面还是移动端登录,都需要使用该接口来调用处理登录的逻辑。
.usernameParameter
和.passwordParameter
定义了认证所需的用户名和密码的参数名,默认用户名参数是username,密码参数是password,开发者在这里可以自定义。
.successHandler
中则创建了一个匿名内部类来定义登录成功的处理逻辑。用户登录成功后可以跳转到某一个页面,也可以返回一段JSON,这个需要结合实际的情况来决定。这里假设返回一段JSON,那么用户登录成功后,返回一段成功的JSON。onAuthenticationSuccess
方法的第三个参数authentication
一般用来获取当前登录用户的信息,在登录成功后,可以获取到当前登录用户的信息一起返回给客户端。
同理在.failureHandler
中则创建了一个匿名内部类来定义登录失败的处理逻辑。用户登录失败后可以跳转到某一个页面,也可以返回一段JSON,这里依旧假设返回一段JSON,不同的是,登录失败的回调方法中有一个AuthenticationException
参数,通过这个异常参数可以获取登录失败的原因,进而给用户一个较为明确的提示,之后用户可以根据该提示进行一些修改操作,进而完成登录操作。
配置完成后,接下来启动项目,开始进行登录测试,这是登录成功的图片:
而这是登录失败的示意图,可以看到之所以登录失败是因为密码或者账号输入错误:
注销登录配置 在前面如果开发者想要切换不同的用户进行登录,必须通过停止和重启项目才能完成,接下来介绍如何注销登录,其实这个也很简单,只需要提供简单的配置即可。继续修改configure(HttpSecurity httpSecurity)
方法,只需要在该方法的后面添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 .and() .logout() .logoutUrl("/logout") .clearAuthentication(true) .invalidateHttpSession(true) .addLogoutHandler(new LogoutHandler() { @Override public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) { } }).logoutSuccessHandler(new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.sendRedirect("/login_page"); } }).and();
同样也简单解释一下上述代码的含义:
.logout()
表示开启注销登录的配置。.logoutUrl("/logout")
表示配置注销登录请求URL为/logout
,默认也是/logout
。.clearAuthentication(true)
表示是否清除身份认证信息,默认为true,表示清除。.invalidateHttpSession(true)
表示是否使Session失效,默认为true。.addLogoutHandler
表示配置一个LogoutHandler
,开发者可以在LogoutHandler
中完成一些数据清除的工作,如Cookie的清除等,SpringSecurity提供了一些常见的实现,如下图所示:
接着配置了一个.logoutSuccessHandler
拦截,开发者可以在这里配置当注销成功后应当执行的业务逻辑,如返回一段JSON提示或者是跳转到登录页面等,这些都是需要结合具体场景来的。
多个HttpSecurity 如果业务较为复杂,开发者也可以配置多个HttpSecurity,以实现对WebSecurityConfigurerAdapter
的多次扩展。新建一个MultiHttpSecurityConfig
类,其中的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Configuration public class MultiHttpSecurityConfig { @Bean PasswordEncoder passwordEncoder(){ return NoOpPasswordEncoder.getInstance(); } @Autowired protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("admin").password("1234").roles("ADMIN","USER") .and() .withUser("envy").password("1234").roles("USER"); } @Configuration @Order(1) public static class AdminSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.antMatcher("/admin/**").authorizeRequests() .anyRequest().hasRole("ADMIN"); } } @Configuration public static class OtherSecurityConfig extends WebSecurityConfigurerAdapter{ @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginProcessingUrl("/login") .permitAll() .and() .csrf() .disable(); } } }
在上述类中,配置多个HttpSecurity时,MultiHttpSecurityConfig
不需要继承WebSecurityConfigurerAdapter
,只需在MultiHttpSecurityConfig
类中创建静态内部类继承WebSecurityConfigurerAdapter
即可,静态内部类上也需要添加@Configuration
注解,且由于存在两个HttpSecurity,因此需要使用@Order
注解,@Order
注解表示该配置的优先级,数字越小优先级越大,未加@Order
注解的配置优先级最小。AdminSecurityConfig
静态内部类主要用于处理/admin/**
模式的URL,其他模式的URL则在OtherSecurityConfig
静态内部类中配置的HttpSecurity中进行处理。
密码加密 (1)为什么需要密码加密? 如果开发者直接在数据库中存入明文密码,那么当数据库被不法分子攻破后密码就毫无保密可言,因此对密码进行加密是对密码进行二次保护。(2)加密方案。 密码加密一般会用到散列函数,又称散列算法,哈希函数,这是一种从任何数据中创建数字“指纹”的方法。散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来,然后将数据进行打乱混合,重新创建一个散列值。散列值通常用一个短的随机字母和数字组成的字符串来代表。好的散列函数在输入域中很少出现散列冲突。在散列表和数据处理中,不抑制冲突来区别数据会使得数据库记录更难找到。常用的散列函数有MD5消息摘要算法、安全散列算法(Secure Hash Algorithm)。 但是仅仅使用散列函数还不够,为了增加密码的安全性,一般在密码加密过程中还需要加盐,所谓的盐可以是一个随机数,也可以是用户名,加盐之后,即使密码明文相同的用户生成的密码,密文也是不相同的,这极大地提高了密码的安全性。但是传统的加盐方式需要在数据库中有专门的字段来记录盐值,这个字段可能使用用户名字段(因为用户名唯一),也可能是一个专门记录盐值的字段,这样的配置较为繁琐。SpringSecurity提供了多种密码加密方案,官方推荐使用BCryptPasswordEncoder
,BCryptPasswordEncoder
使用BCrypt强哈希函数,开发者在使用时可以选择提供stength和SecureRandom实例。其中stength越大,密钥的迭代次数越多,密钥的迭代次数为2^strength。stength取值在4~31之间,默认为10,以上就是通常意义上的加密方案。(3)实践。 在SpringBoot中配置密码加密非常简单,只需要修改上文配置的PasswordEncoder
这个Bean的实现即可,相应的代码为:
1 2 3 4 5 //使用BCryptPasswordEncoder哈希函数进行密码加密 @Bean PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(10); }
创建BCryptPasswordEncoder
时传入的参数10就是stength,即密钥的迭代次数(也可以不配置,默认stength的值就是10)。同时请注意配置的内存用户的密码不再是1234,而是经BCryptPasswordEncoder
加密后的密码,相应的代码为:
1 2 3 4 5 6 7 8 9 10 //使用BCryptPasswordEncoder哈希函数进行密码加密后对密码进行保存操作 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("root").password("$2a$10$LE86rB.N5wYXAVRkLIEtGuLnPzvccwWN2/V5ZWa6g0Drk3WfvV5ou").roles("ADMIN","DBA") .and() .withUser("admin").password("$2a$10$/BOtEyJ62TbMea8BcnDgMOjQU58dlWm.l0QO5G7BEr5LhQ3F0GRf.").roles("ADMIN","USER") .and() .withUser("envy").password("$2a$10$5KYz93AK76T6uixVe/mPlempnSRwo2VxAfygj/q4v5A66AjWnc.kC").roles("USER"); }
这里的密码就是使用BCryptPasswordEncoder
加密后的密码,虽然admin,root,envy加密后的密码不一样,但是明文都是1234。配置完成后,使用admin/1234、root/1234或者envy/1234就可以实现登录了。
此处的例子使用了配置在内存中的用户,一般情况下,用户信息都是存储在数据库中的,因此需要在用户注册时对密码进行加密,这里提供一段伪代码:
1 2 3 4 5 6 7 8 @Service public class RegisterService { public int register(String username,String password){ BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(10); String encodePassword = bCryptPasswordEncoder.encode(password); return saveToDb(username,encodePassword ); } }
用户将密码从前端传过来之后,通过调用BCryptPasswordEncoder
实例中的encode方法对密码进行加密处理,加密完成后将密文存入数据库即可。
方法安全 前面介绍的认证都是基于URL这种方式,其实开发者也可以通过注解来灵活配置方法安全,不过想要使用相关注解,首先需要通过@EnableGlobalMethodSecurity
注解来开启基于注解的安全配置。新建一个MyAnnoWebSecurityConfig
类,里面的代码为:
1 2 3 4 @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true) public class MyAnnoWebSecurityConfig { }
简单解释一下上述代码的含义:prePostEnabled
会解锁@PreAuthorize
和@PostAuthorize
这两个注解,顾名思义,@PreAuthorize
注解会在方法执行前进行验证,而@PostAuthorize
注解则在方法执行后进行验证。securedEnabled=true
会解锁@Secured
注解。
开启注解安全配置后,接下来创建一个AnnotationService进行测试,请注意事先需要注释掉前面基于URL模式的相关配置信息。相应的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Service public class AnnotationService { @Secured("ROLE_ADMIN") public String admin(){ return "hello,admin"; } @PreAuthorize("hasRole('ADMIN')and hasRole('DBA')") public String dba(){ return "hello,dba"; } @PreAuthorize("hasAnyRole('ADMIN','DBA','USER')") public String user(){ return "hello,user"; } }
接着新建AnnotationController
类,其中的代码为:
1 2 3 4 5 6 7 8 9 10 @RestController public class AnnotationController { @Autowired private AnnotationService annotationService; @GetMapping("/admin") public String admin(){ return annotationService.admin(); } }
然后运行项目开始进行测试,这里就不过多赘述了。
基于数据库的认证 前面学习的认证都是在内存中完成的,在真实开发过程中,用户的基本信息及角色都是保存在数据库中,因此需要从数据库中进行认证。接下来就向大家介绍如何使用数据库中的数据进行认证与授权。第一步,设计数据表。 首先需要设计一个基本的用户角色表,如下图所示,一个三张表,分别是用户表,角色表,用户角色关联表:
考虑到性能问题,这里使用Mybatis,因此需要提前创建数据库和表,使用的SQL语句为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 drop database if exists sqlsecurity; create database sqlsecurity; use sqlsecurity; drop table if exists user; create table user( id int(11) not null primary key auto_increment, username varchar(32) not null comment '用户名', password varchar(255) not null comment '密码', enabled tinyint(1) default 1 comment '是否被禁用,1表示否,0表示是,默认1', locked tinyint(1) default 0 comment '是否被锁住,1表示是,0表示否,默认0' )engine=innodb,charset=utf8; drop table if exists role; create table role( id int(11) not null primary key auto_increment, name varchar(32) not null comment '角色', name_note varchar(255) not null comment '角色说明' )engine=innodb,charset=utf8; drop table if exists user_role; create table user_role( id int(11) not null primary key auto_increment, uid int(11)not null, rid int(11) not null )engine=innodb,charset=utf8; alter table user_role add constraint fk_user_user_role foreign key(uid) references user(id); alter table user_role add constraint fk_role_user_role foreign key(rid) references role(id);
为了后续测试,可以先往其中增加一些数据,具体的自行添加,这里贴一下笔者的数据示意图:
请注意角色名必须有一个默认的前缀ROLE_
,这一点和前面内存的配置方式是不相同的。第二步,创建项目。 使用spring Initializr
构建工具构建一个SpringBoot的Web应用,名称为sqlspringsecurity,然后添加如下依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--添加security依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--添加数据库驱动依赖--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--添加数据库连接池依赖--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.25</version> </dependency> <!--添加mybatis依赖--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <!--添加lombok依赖--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
第三步,配置数据库。 在application.properties
数据库配置文件中添加数据库连接配置信息:
1 2 3 4 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.url=jdbc:mysql://127.0.0.1:3306/sqlsecurity?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC spring.datasource.username=root spring.datasource.password=1234
第四步,创建实体类。 新建pojo包,并在其中根据数据库表来创建用户表和角色表所对应的实体类。其中Role.java
类中的代码为:
1 2 3 4 5 6 @Data public class Role { private Integer id; private String name; private String nameNote; }
而User.java
类中的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 @Data public class User implements UserDetails { private Integer id; private String username; private String password; @Getter(value = AccessLevel.NONE) private Boolean enabled; private Boolean locked; private List<Role> roles; @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<SimpleGrantedAuthority> authorities = new ArrayList<>(); for(Role role:roles){ authorities.add(new SimpleGrantedAuthority(role.getName())); } return authorities; } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return !locked; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return enabled; } }
请注意这个User必须实现UserDetails接口,且实现其中的7个接口方法,如下表所示:
方法名
解释
getAuthorities
获取当前用户所具有的角色信息
getPassword
获取当前用户对象的密码
getUsername
获取当前用户对象的用户名
isAccountNonExpired
当前账户是否未过期
isAccountNonLocked
当前账户是否未锁定
isCredentialsNonExpired
当前账户密码是否未过期
isEnabled
当前账户是否可用
用户可以根据实际情况设置这7个方法的返回值,因为默认情况下不需要开发者自己进行密码角色等信息的对比,开发者只需要提供相关信息即可。如getPassword
方法返回的密码和用户输入的密码不匹配,会自动抛出BadCredentialsException
异常,isAccountNonExpired
方法返回了false,会自动抛出AccountExpiredException
异常,因此对于开发者而言,只需要按照数据库中的数据在这里返回相对应的配置即可。本例子中的数据库只设置了enabled和locked字段,故账户未过期和密码未过期这两个方法都是直接返回了true。getAuthorities
方法用来获取当前用户所具有的角色信息,在本例子中用户所具有的角色存储在roles属性中,因此该方法可以直接遍历roles属性,然后构造SimpleGrantedAuthority
集合并返回。
同时在enabled属性上必须添加@Getter(value = AccessLevel.NONE)
注解,也就是不生成getter方法,因为此处的isEnabled方法实际上就是enabled属性的getter方法。
第五步,创建数据库访问层。 新建mapper包,接着在里面新建一个UserMapper接口(注意名称必须是实体类大写+Mapper
这种格式),里面的代码为:
1 2 3 4 5 @Mapper public interface UserMapper { User loadUserByUsername(String username); List<Role> getUserRolesByUid(Integer uid); }
第六步,创建UserMapper.xml文件 。请注意必须在与UserMapper接口相同的包内新建UserMapper.xml
文件,里面的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.envy.sqlspringsecurity.mapper.UserMapper"> <select id="loadUserByUsername" parameterType="String" resultType="com.envy.sqlspringsecurity.pojo.User"> select * from user where username = #{username} </select> <select id="getUserRolesByUid" resultType="com.envy.sqlspringsecurity.pojo.Role"> select * from role r, user_role ur where ur.uid= #{uid} and r.id = ur.rid </select> </mapper>
第七步,创建Service层。 在service包内新建UserService
类,里面的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Service public class UserService implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.loadUserByUsername(username); if(user==null){ throw new UsernameNotFoundException("账户不存在"); } user.setRoles(userMapper.getUserRolesByUid(user.getId())); return user; } }
请注意这里的UserService类也需要实现UserDetailsService
接口,并实现其中的loadUserByUsername
方法,该方法的参数就是用户登录时输入的用户名,通过用户名去数据库中查找用户,如果没有查到用户就抛出一个账户不存在的异常,如果查到了用户就继续查找用户所具有的角色信息,并将获取到的user对象返回,再由系统提供的DaoAuthenticationProvider
类去比对密码是否正确。特别注意这个loadUserByUsername
方法,它是在用户登录的时候自动调用的。
第八步,配置Spring Security。 新建config包,并在其中新建WebSecurityConfig
类,里面的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Bean PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(10); } @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService); } @Override public void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.authorizeRequests() .antMatchers("/admin/**").hasRole("admin") .antMatchers("/db/**").hasRole("dba") .antMatchers("/user/**").hasRole("user") .anyRequest() .authenticated() .and().formLogin().loginProcessingUrl("/login").permitAll() .and().csrf().disable(); } }
这里大部分的配置和前面基于内存的验证方式的配置是一致的,不同的就是没有配置内存用户,而是将刚刚创建好的UserService
配置到AuthenticationManagerBuilder
中。
第九步,配置pom.xml文件,这一步很重要。 在Maven工程中,XML配置文件建议写在resources目录下,但是很明显上述的BookMapper.xml
文件写在了mapper包内,且不在resources目录下,此时运行项目肯定会抛出mapper文件找不到的异常,因为Maven运行时会忽略包内的xml文件,因此需要在pom.xml文件中重新指明资源文件的位置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <build> <!--使用mybatis时需要手动指明xml的位置--> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> <resource> <directory>src/main/resources</directory> </resource> </resources> </build>
第十步,创建Controller层。 在controller包内新建UserController类,里面的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @RestController public class UserController { @GetMapping("/admin/hello") public String admin(){ return "hello,admin!"; } @GetMapping("/user/hello") public String user(){ return "hello,user!"; } @GetMapping("/db/hello") public String dba(){ return "hello,dba!"; } @GetMapping("/hello") public String hello(){ return "hello!"; } }
第十一步,测试。 运行项目 ,在浏览器中输入http://localhost:8080/admin/hello
链接时,会里面跳转到http://localhost:8080/login
链接,且只有admin和root用户才能访问到该页面,其余的链接和这个非常相似这里就不过多赘述。
高级配置 角色继承 在前面定义了三种角色:ROLE_dba、ROLE_admin和ROLE_user,但是三种角色之间不具备任何关系,一般来说角色之间是有关系的,如ROLE_admin一般既有admin的权限,又具有user的权限。那么如何配置这种继承关系呢?在Spring Security中只需要开发者提供一个RoleHierarchy
即可。以前面数据库认证的例子为例,假设ROLE_dba具有所有的权限,ROLE_admin也具有ROLE_user的权限,而ROLE_user则是一个公共的角色,即ROLE_admin继承ROLE_user、ROLE_dba继承ROLE_admin,要想描述这种继承关系,只需要开发者在SpringSecurity的配置类(此处是WebSecurityConfig类)中提供一个RoleHierarchy
即可,相应的代码为:
1 2 3 4 5 6 7 @Bean RoleHierarchy roleHierarchy(){ RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl(); String hierarchy = "ROLE_dba> ROLE_admin ROLE_admin>ROLE_user"; roleHierarchy.setHierarchy(hierarchy); return roleHierarchy; }
配置完RoleHierarchy
之后,具有ROLE_dba角色的用户就可以访问到所有资源,而ROLE_admin角色的用户也可以访问具有ROLE_user角色才能访问的资源。开发者可以自行测试一下上述配置是否成功生效,此处笔者就不再赘述了。
动态配置权限 使用HttpSecurity配置的认证授权规则还不够灵活,无法实现资源和角色之间的动态调整,要实现动态配置URL权限,就需要开发者自定义权限配置。此处依然基于前面数据库的认证案例进行权限的动态配置。
第一步,数据库设计。 此处的数据库依旧是前面的sqlsecurity,现在在该基础上新增两张表:资源表和资源角色关联表,其中资源表定义了用户能够访问的URL模式,资源角色表则定义了访问该模式的URL应当需要怎样的角色,使用的SQL语句如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 use sqlsecurity; drop table if exists menu; create table menu( id int(11) not null primary key auto_increment, pattern varchar(32) not null comment 'URL模式' )engine=innodb,charset=utf8; drop table if exists menu_role; create table menu_role( id int(11) not null primary key auto_increment, mid int(11)not null, rid int(11) not null )engine=innodb,charset=utf8; alter table menu_role add constraint fk_menu_menu_role foreign key(mid) references menu(id); alter table menu_role add constraint fk_role_menu_role foreign key(rid) references role(id);
为了后续测试,可以先往其中增加一些数据,具体的自行添加,这里贴一下笔者的数据示意图:
第二步创,创建实体类。 在pojo包中根据数据库表来创建资源表所对应的实体类。其中Menu.java
类中的代码为:
1 2 3 4 5 6 @Data public class Menu { private Integer id; private String pattern; private List<Role> roles; }
第三步,创建数据库访问层。 新建一个MenuMapper接口(注意名称必须是实体类大写+Mapper
这种格式),里面的代码为:
1 2 3 4 @Mapper public interface MenuMapper { List<Menu> getAllMenus(); }
第四步,创建MenuMapper.xml文件 。请注意必须在与MenuMapper接口相同的包内新建MenuMapper.xml
文件,里面的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.envy.sqlspringsecurity.mapper.MenuMapper"> <resultMap id="BaseResultMap" type="com.envy.sqlspringsecurity.pojo.Menu"> <id property="id" column="id"/> <result property="pattern" column="pattern"/> <collection property="roles" ofType="com.envy.sqlspringsecurity.pojo.Role"> <id property="id" column="rid"/> <result property="name" column="rname"/> <result property="nameNote" column="rnameNote"/> </collection> </resultMap> <select id="getAllMenus" resultMap="BaseResultMap"> select m.*,r.id as rid,r.name as rname,r.name_note as rnameNote from menu m left join menu_role mr on m.id=mr.mid left join role r on mr.rid=r.id </select> </mapper>
**第五步,自定义FilterInvocationSecurityMetadataSource
**。要实现动态配置权限。首先要自定义FilterInvocationSecurityMetadataSource
,Spring Security中通过FilterInvocationSecurityMetadataSource
接口中的getAttributes方法来确定一个请求需要哪些角色,FilterInvocationSecurityMetadataSource
接口的默认实现类是DefaultFilterInvocationSecurityMetadataSource
,开发者可以参考该实现类中的方法实现来自定义实现自己CustomFilterInvocationSecurityMetadataSource
类的方法。新建component包,并在其中创建CustomFilterInvocationSecurityMetadataSource
类文件,里面的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Component public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { AntPathMatcher antPathMatcher = new AntPathMatcher(); @Autowired MenuMapper menuMapper; @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { String requestUrl = ((FilterInvocation)object).getRequestUrl(); List<Menu> allMenus = menuMapper.getAllMenus(); for(Menu menu:allMenus){ if(antPathMatcher.match(menu.getPattern(),requestUrl)){ List<Role> roles = menu.getRoles(); String [] roleArr = new String[roles.size()]; for(int i =0;i<roleArr.length;i++){ roleArr[i] = roles.get(i).getName(); } return SecurityConfig.createList(roleArr); } } return SecurityConfig.createList("ROLE_LOGIN"); } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> aClass) { return FilterInvocation.class.isAssignableFrom(aClass); } }
解释一下上述代码的含义:(1)开发者自定义一个CustomFilterInvocationSecurityMetadataSource
类且实现FilterInvocationSecurityMetadataSource
接口,同时实现了其接口中的3个方法,不过主要是对getAttributes方法进行了实现,该方法的参数是一个FilterInvocation
对象,开发者可以从FilterInvocation
对象的getRequestUrl
方法中提取出当前请求的URL,getAttributes方法的返回值是一个Collection<ConfigAttribute>
表示当前请求URL所需的角色。(2)使用AntPathMatcher antPathMatcher = new AntPathMatcher();
创建了一个AntPathMatcher
对象,主要用来实现ant风格的URL匹配。(3)调用((FilterInvocation)object).getRequestUrl()
方法来从参数中提取出当前请求的URL。(4)List<Menu> allMenus = menuMapper.getAllMenus();
表示从数据库中获取所有的资源信息,在此例子中是指menu表以及menu所对应的role。在真实项目环境中,开发者可以将资源信息缓存在Redis或者其他缓存数据库中。(5)接下来的for循环用于遍历资源信息,遍历过程中获取当前请求的URL所需要的角色信息并返回。如果当前请求的URL在资源表中不存在相应的模式,就假设该请求登录后即可访问,即直接返回ROLE_LOGIN
。(6)getAllConfigAttributes
方法用来返回定义好的权限资源,SpringSecurity在启动时会校验相关配置是否正确,如果不需要校验,那么该方法直接返回null即可。(7)supports
方法返回类对象是否支持校验。
第六步,自定义AccessDecisionManager 当一个请求走完FilterInvocationSecurityMetadataSource
中的getAttributes
方法后,接下来就会来到AccessDecisionManager
类中进行角色信息的比对。在component包中创建CustomAccessDecisionManager
类文件,里面的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Component public class CustomAccessDecisionManager implements AccessDecisionManager { @Override public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException { Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); for(ConfigAttribute configAttribute:collection){ if("ROLE_LOGIN".equals(configAttribute.getAttribute()) && authentication instanceof UsernamePasswordAuthenticationToken){ return; } for(GrantedAuthority authority:authorities){ if(configAttribute.getAttribute().equals(authority.getAuthority())){ return; } } } throw new AccessDeniedException("权限不足"); } @Override public boolean supports(ConfigAttribute configAttribute) { return true; } @Override public boolean supports(Class<?> aClass) { return true; } }
解释一下上述代码的含义:(1)开发者自定义一个CustomAccessDecisionManager
类且实现AccessDecisionManager
接口中的三个方法,最主要的还是decide方法,在该方法中判断当前登录用户是否具备当前请求URL所需要的的角色信息,如果不具备,就抛出AccessDeniedException
异常,否则不做任何事情即可。(2)decide方法有三个参数,第一个参数包含当前登录用户的信息;第二个参数则是一个FilterInvocation
对象,可以获取当前请求信息等;第三个参数就是CustomFilterInvocationSecurityMetadataSource
类中getAttributes
方法的返回值,即当前请求URL所需要的角色。(3)decide方法用于进行角色信息对比,如果需要的角色是ROLE_LOGIN
,说明当前请求的URL用户只需登录即可访问,如果authentication
是UsernamePasswordAuthenticationToken
的实例,那么说明当前用户已经登录,该方法到此结束,否则进入正常的判断流程,如果当前用户具备当前请求需要的角色,那么方法也结束。
第七步,配置自定义类 在前面定义的WebSecurityConfig
类中配置上述自定义的两个类。请注意主要是对configure(HttpSecurity httpSecurity)
方法进行修改和新增上述两个类的配置信息,相应的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Bean PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(10); } @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService); } @Bean RoleHierarchy roleHierarchy(){ RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl(); String hierarchy = "ROLE_dba> ROLE_admin ROLE_admin>ROLE_user"; roleHierarchy.setHierarchy(hierarchy); return roleHierarchy; } //在数据库中定义URL模式 @Override public void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.authorizeRequests() .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O o) { o.setSecurityMetadataSource(cfisms()); o.setAccessDecisionManager(cadm()); return o; } }) .and() .formLogin() .loginProcessingUrl("/login").permitAll() .and() .csrf().disable(); } @Bean CustomFilterInvocationSecurityMetadataSource cfisms(){ return new CustomFilterInvocationSecurityMetadataSource(); } @Bean CustomAccessDecisionManager cadm(){ return new CustomAccessDecisionManager(); } }
此处仅仅是对前述WebSecurityConfig
定义的补充,特别是对configure(HttpSecurity httpSecurity)
方法的实现进行了修改,且添加了两个Bean。在定义FilterSecurityInterceptor
时,将前述定义的两个实例作为属性设置进去。
通过上面的配置就实现了动态配置权限,权限和资源的关系可以在muen_role
表中动态来调整。运行项目,接下来就是测试环节,考虑到这里的测试和前面的测试相差不大,因此就忽略测试环节。