1、Spring Security 概述
1.1、Spring Security 简介
Spring Security 是 Spring 家族中的成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。
在对与安全方面的两个主要区域是“认证”和“授权”(或者访问控制),一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,这两点也是 Spring Security 重要核心功能。
- 用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。通俗点说就是系统认为用户是否能登录
- 用户授权指的是验证某个用户是否有权限执行某个 *** 作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。通俗点讲就是系统判断用户是否有权限去做某些事情。
1.2、Spring Security 与 Shiro
SpringSecurity 特点:
- 和 Spring 无缝整合。
- 全面的权限控制。
- 专门为 Web 开发而设计。
- 旧版本不能脱离 Web 环境使用。
- 新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独引入核心模块就可以脱离 Web 环境。
- 重量级。
Shiro 特点:
- 轻量级。Shiro 主张的理念是把复杂的事情变简单。针对对性能有更高要求的互联网应用有更好表现。
- 通用性。
- 好处:不局限于 Web 环境,可以脱离 Web 环境使用。
- 缺陷:在 Web 环境下一些特定的需求需要手动编写代码定制。
相对于 Shiro,在 SSM 中整合 Spring Security 都是比较麻烦的 *** 作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了自动化配置方案,可以使用更少的配置来使用Spring Security。
因此,一般来说,常见的安全管理技术栈的组合是这样的:
- SSM + Shiro
- Spring Boot/Spring Cloud + Spring Security
shiro详解:参考该文,单击前往
2、Hello World
使用springboot集成security安全框架,直接快速初始化一个springboot项目,在这里直接将对应的依赖进行添加(也可以在项目的pom当中添加),之后直接将项目构建出来,等待jar包拉取完。
而后直接写一个controller来进行测试(如下代码所示),之后直接启动项目访问这个controller的路由,会被转到security的登录页面,需要登录之后才能够访问到对应的路由,这里的账号为user,密码在项目启动的日志当中会输出,并且每次启动的时候密码都回发生变化。
@RestController
public class HelloController {
@GetMapping("/getSecurity")
public String hello(){
return "hello security";
}
}
3、SpringSecurity Web 权限方案
3.1、设置登录校验的账号和密码
方案一:直接在配置文件当中进行配置账号和密码:
spring.security.user.name=yueyue
spring.security.user.password=123456
方案二:使用配置类进行设置:新建配置类继承至WebSecurityConfigurerAdapter 重写 configure 方法
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder password(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("123456");
auth.inMemoryAuthentication().withUser("yueyue").password(password).roles("admin");
}
方案三:自定义账号密码:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder password(){
return new BCryptPasswordEncoder();
}
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
}
@Service("userDetailsService")
public class UserService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User("yueyue",new BCryptPasswordEncoder().encode("123456"),auths);
}
}
3.2、查数据库进行认证
在这里首先添加相对应的依赖,这里使用mybatis-plus来对数据库进行 *** 作。
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.1.20version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.0.5version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
并且在这里我们使用一个表用来记录账号密码,以及对应的实体类、mapper接口、数据库连接配置等等。
# 连接数据库配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springboot?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
# 实体类
@Data
@TableName("user1")
public class User {
private Long id;
private String userName;
private String userPassword;
}
# mapper接口
@Repository
public interface UserMapper extends BaseMapper {
}
之后只需要修改前面进行获取账号密码的方法,代码如下:
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<com.lzq.entity.User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_name",username);
com.lzq.entity.User user = userMapper.selectOne(queryWrapper);
if(user == null){
throw new UsernameNotFoundException("用户名不存在!");
}
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User(user.getUserName(),new BCryptPasswordEncoder().encode(user.getUserPassword()),auths);
}
3.3、自定义登录页面
在config当中还需对configure方法进行重写:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
// 登录页面设置
.loginPage("/login.html")
// 登录访问路径
.loginProcessingUrl("/user/login")
// 登录成功后跳转路径
.defaultSuccessUrl("/getSecurity").permitAll()
// 不需要验证授权
.and().authorizeRequests()
.antMatchers("/info").permitAll()
.anyRequest().authenticated()
;
http.csrf().disable();
// 403没有权限访问页面将跳转到403.html页面
http.exceptionHandling().accessDeniedPage("/403.html");
}
这里使用到一个login.html来作为登录的首页,简单的对html加上个form表单:
<form action="/user/login" method="post">
用户名:<input type="text" name="username"/>
<br/>
密码:<input type="password" name="password"/>
<br/>
<input type="submit" value="login">
form>
在进行配置之后,每当提交过来的username和password会和之前设置的进行比较,如果相同的话则会跳转到getSecurity表示登录成功,其余皆为405。
并且在这里的表单提交给的参数也必须是username和password,这是因为在进行账号密码校验的时候会通过UsernamePasswordAuthenticationFilter过滤器,而在过滤器当中进行取值的时候最终会获取usernam和password变量。
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
而这里同样的也可以不用username和password这两个变量,在form表单当中重新指定两个变量用来传递参数,比如说改成account和pass,那么我们只需要在配置类当中加入
3.4、基于角色或权限进行访问控制
- hasAuthority 方法:如果当前的主体具有指定的权限,则返回 true,否则返回 false
- hasAnyAuthority 方法:如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回true
- hasRole 方法:如果用户具备给定角色就允许访问,否则出现 403。
- hasAnyRole方法:同上,多个角色
还是修改前面的配置类来进行校验验证,添加两个路由访问规则,分别给上admin和manager角色才能够对路由进行访问。
http.formLogin()
// 登录页面设置
.loginPage("/login.html")
// 登录访问路径
.loginProcessingUrl("/user/login")
// 登录成功后跳转路径
.defaultSuccessUrl("/getSecurity").permitAll()
// 不需要验证授权
.and().authorizeRequests()
.antMatchers("/info").permitAll()
// admin角色才能访问
.antMatchers("/admin").hasAuthority("admin")
// admin或者manager其中一个角色即可访问
.antMatchers("/adminManager").hasAnyAuthority("admin","manager")
.antMatchers("/role").hasRole("role")
.antMatchers("/anyrole").hasAnyRole("role","admin")
.anyRequest().authenticated()
;
http.csrf().disable();
而对于角色的给定,在前面实现UserDetailsService接口的实现类的loadUserByUsername方法当中进行指定。
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<com.lzq.entity.User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_name",username);
com.lzq.entity.User user = userMapper.selectOne(queryWrapper);
if(user == null){
throw new UsernameNotFoundException("用户名不存在!");
}
// 指定角色
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
// 而对于hasRole、hasAnyRole 方法的角色需要加上ROLE_前缀
// List auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_role");
return new User(user.getUserName(),new BCryptPasswordEncoder().encode(user.getUserPassword()),auths);
}
在这里我们可以直接追到这几个方法的内部查看区别,可以看到对于hasAuthority和hasAnyAuthority是直接使用角色的名称进行 *** 作,而hasRole和hasAnyRole是加上了一个前缀再进行 *** 作。
private static String hasAuthority(String authority) {
return "hasAuthority('" + authority + "')";
}
private static String hasAnyAuthority(String... authorities) {
String anyAuthorities = StringUtils.arrayToDelimitedString(authorities, "','");
return "hasAnyAuthority('" + anyAuthorities + "')";
}
private static String hasRole(String rolePrefix, String role) {
Assert.notNull(role, "role cannot be null");
Assert.isTrue(rolePrefix.isEmpty() || !role.startsWith(rolePrefix), () -> "role should not start with '"
+ rolePrefix + "' since it is automatically inserted. Got '" + role + "'");
return "hasRole('" + rolePrefix + role + "')";
}
private static String hasAnyRole(String rolePrefix, String... authorities) {
String anyAuthorities = StringUtils.arrayToDelimitedString(authorities, "','" + rolePrefix);
return "hasAnyRole('" + rolePrefix + anyAuthorities + "')";
}
3.5、注解使用
- @Secured:判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“
- @PreAuthorize:注解适合进入方法前的权限验证, @PreAuthorize 可以将登录用户的 roles/permissions 参数传到方法中。
- @PostAuthorize:在方法执行后再进行权限验证,适合验证带有返回值的权限。
- @PreFilter: 进入控制器之前对数据进行过滤
- @PostFilter :权限验证之后对数据进行过滤
// 在启动类上加上注解
@EnableGlobalMethodSecurity(securedEnabled=true)
// 在接口上加上注解
@GetMapping("/autoRole")
@Secured("ROLE_role")
public String AutoRole(){
return "auto role";
}
// 加在启动类上
@EnableGlobalMethodSecurity(securedEnabled=true,prePostEnabled = true)
// 测试接口
@GetMapping("/autoRole")
@PreAuthorize("hasAnyRole('ROLE_role')")
public String AutoRole(){
return "auto role";
}
// 加在启动类上
@EnableGlobalMethodSecurity(securedEnabled=true,prePostEnabled = true)
// 测试接口
@GetMapping("/autoRole")
@PostAuthorize("hasAnyAuthority('admins')")
public String AutoRole(){
System.out.println("PostAuthorize");
return "auto role";
}
// 进入控制器之前对数据进行过滤
@PostMapping("/getPreFilter")
@PreAuthorize("hasRole('ROLE_管理员')")
@PreFilter(value = "filterObject.id%2==0")
@ResponseBody
public List<UserInfo> getTestPreFilter(@RequestBody List<UserInfo> list){
list.forEach(t-> {
System.out.println(t.getId()+"\t"+t.getName());
});
return list;
}
// 表达式中的 filterObject 引用的是方法返回值 List 中的某一个元素
@GetMapping("/getAll")
@PreAuthorize("hasRole('ROLE_role')")
@PostFilter("filterObject.name == 'admin1'")
@ResponseBody
public List<UserInfo> getAllUser(){
ArrayList<UserInfo> list = new ArrayList<>();
list.add(new UserInfo(1,"admin1","6666"));
list.add(new UserInfo(2,"admin2","888"));
return list;
}
3.6、登出 *** 作
在进行登出时,只需要在配置类上加入登出对应的配置即可。在访问/logout路由的时候,会进行登出 *** 作,销毁session,页面跳转到/index页面
http.logout().logoutUrl("/logout").logoutSuccessUrl("/index").permitAll();
3.7、RememberMe 自动登录
在进行登录验证的时候,会通过RememberMeAuthenticationFilter过滤器。而在通过过滤器之后,会获取token值进行校验,而在之前的登录过程当中会通过JdbcTokenRepositoryImpl实现类,该类主要是对每次登录之后将生成的token值写入到数据库当中,而后续的自动登录的时候,会获取浏览器的token和数据库当中存的token进行比较验证。
新增表表结构,用来记录登录信息以及token,以及springboot项目对数据库的一些相关配置。
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE
CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
这里对于表数据的 *** 作,在JdbcTokenRepositoryImpl实现类当中都有,也不需要我们自己去对数据库进行 *** 作,我们只需要在配置类当中加入相关的配置即可。如下:(部分代码)
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
// 赋值数据源
jdbcTokenRepository.setDataSource(dataSource);
// 自动创建表,第一次执行会创建,以后要执行就要删除掉!
// jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
// 在configure方法当中指定rememberMe
and().rememberMe().tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60).userDetailsService(userDetailsService)
并且在登录页面上可以加上一个checkbox复选框,用来标识rememberMe,这里的name的值也必须是remember-me。
<input type="checkbox"name="remember-me"title="记住密码"/><br/>
之后进行测试,在进行登录的时候,当认证通过之后,会将的搭配的token值写入到数据库当中,并且会表示该token的过期时间。而当这个时候,我们关掉浏览器之后直接访问路由(非登录路由),还是可以直接进行访问的,这是因为我们的请求当中还带上了token,token校验通过之后还是可以直接进去的,而当token过期之后再进行访问,又会直接给转到登录页面。
3.8、CSRF 跨域
跨站请求伪造,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的 *** 作的攻击方法。跟跨网站脚本(XSS)相比,XSS利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。
可以使用一下语句进行关闭跨域检查:
http.csrf().disable();
4、SpringSecurity 微服务权限方案
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)