Spring Security 是Spring Resource 社区的一个安全组件.Sping Secuity为JavaEE企业级开发提供了全面的安全防护,安全防护是一个不断变化的目标,Spring Security通过版本不断迭代来实现这一目标。Spine Sceunt采用"安全层”的概念,使每一层都尽可能安全,连续的安全层可以达到全面的防护。Spring Security可以在Contoller层、Service层,DAO层等以加注解的方式来保护应用程序的安全,Spring Security 提供了细粒度的权限控制,可以精细到每一个API接口、每一个业务的方法,或者每一个操作数据库的DAO层的方法.Spring Security提供的是应用程序层的安全解决方案,一个系统的安全还需要考患传输层和系统层的安全,例如采用Htpps协议、服务器部署防火墙等。
使用 Spring Securiy有很多原因,其中一个重要原因是它对环境的无依赖性、低代码耦合性。将工程重现部署到一个新的服务器上,不需要为 Spring Security做什么工作。Spring Security 提供了数十个安全模块,模块与模块间的耦合性低,模块之间可以自由组合来实现特定需求的安全功能,具有较高的可定制性。总而言之,Spring Security 具有很好的可复用性和可定制性。
在安全方面,有两个主要的领域,一是“认证”,即你是谁;二是“授权”,即你拥有什么权限,Spring Security 的主要目标就是在这两个领域。“认证”是认证主体的过程,通常是指可以在应用程序中执行操作的用户、设备或其他系统。“授权”是指决定是否允许已认证的主体执行某一项操作。
安全框架多种多样,那为什么选择 Spring Security 作为微服务开发的安全框架呢?JavaEE 有另一个优秀的安全框架 Apache Shiro,Apache Shiro 在企业级的项目开发中十分受欢迎,一般使用在单体服务中。但在微服务架构中,目前版本的 Apache Shiro是无能为力的Spring Security 来自 Spring Resource 社区,采用了注解的方式控制权限,熟悉Spring 的开发者很容易上手Spring Security。另外一个原因就是Spring Security易用与Spring boot工程,也容易集成到Spring Cloud构建的微服务系统中。
总结起来有以下几个特点:
Spring Security 和Spring Boot Security的关系如下:
security需要自己写一个配置类,配置类集成于WebSecurityConfigureAdapter
类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private Environment environment;
@Value("${web.security.user.name}")
private String username;
@Value("${web.security.user.pswd}")
private String password;
@Value("${web.security.user.role}")
private String role;
@Value("${web.security.admin.name}")
private String adminName;
@Value("${web.security.admin.pswd}")
private String adminPswd;
@Value("${web.security.admin.role}")
private String adminRole;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser(username).password("{noop}" + password)
.roles(role.split(","));
auth.inMemoryAuthentication().withUser(adminName).password("{noop}" + adminPswd)
.roles(adminRole.split(","));
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/css/**", "/index").permitAll()
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/admin/**").hasRole("ADMIN")
.and()
.formLogin().loginPage("/login").failureUrl("/login-error")
.and()
.exceptionHandling().accessDeniedPage("/401")
.and()
.logout().logoutSuccessUrl("/");
}
}
这里的私有属性是在config.properties里面配置的用户名、密码与权限的信息。
这里最好不要硬编码。
这个方法中,在内存中创建2个用户的信息,用户的用户名、密码以及密码的加密方式,和其具有的角色。
密码的加密方式:
这个方法里只有短短的两行代码,但是其完成了非常多的操作:
其源码如下
写一个简单的html界面用来标识登陆成功。
使用admin登陆
代码 | 配置内容 |
---|---|
“/css/**”,"/index" | 不需要认证即可访问 |
“/user/**” | user目录下的界面需要验证user角色 |
“/admin” | admin目录下的界面需要验证admin角色 |
formLogin | 表单登陆界面是/login界面 |
failureUrl | 登陆失败的地址是/login-error |
exceptionHandling | 异常会被重定向到401界面 |
logout | 支持注销 |
logoutSuccessUrl | 注销后重定向到/ |
基于3.3.3的配置,实现controller
@Controller
public class MainController {
@RequestMapping("/")
public String root(){
return "redirect:/index";
}
@RequestMapping("/index")
public String index(){
return "index";
}
@RequestMapping("/user/index")
public String userIndex(){
return "user/index";
}
@RequestMapping("/admin/index")
public String adminIndex(){
return "admin/index";
}
@RequestMapping("/login")
public String login(){
return "login";
}
@RequestMapping("/login-error")
public String loginError(Model model){
model.addAttribute("loginError",true);
return "login";
}
@GetMapping("/401")
public String accessDenied(){
return "401";
}
}
login.html
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Login pagetitle>
<base href="/">
<link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}" />
head>
<body>
<h1>Login pageh1>
<p th:if="${loginError}" class="error">用户名或者密码错误!p>
<form th:action="@{/login}" method="post">
<label for="username">用户名label>:
<input type="text" id="username" name="username" autofocus="autofocus"/><br/>
<label for="password">密 码label>:
<input type="password" id="password" name="password" autofocus="autofocus"/><br/>
<input type="submit" value="登录"/>
form>
<p><a th:href="@{/index}">返回首页a> p>
body>
html>
index.html
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<title>Hello Spring Boot Security for indextitle>
<base href="/">
<link rel="stylesheet" href="css/main.css" th:href="@{/css/main.css}"/>
head>
<body>
<h1>Hello Spring Boot Security for indexh1>
<p>这个界面没有受到保护.p>
<div th:fragment="logout" sec:authorize="isAuthenticated()">
登录用户:<span sec:authentication="name"/>
用户角色:<span sec:authentication="principal.authorities"/>
<div>
<form action="#" th:action="@{/logout}" method="post">
<input type="submit" value="登出" />
form>
div>
div>
<ul>
<li>点击<a href="/user/index" th:href="@{/user/index}">去/user/index被保护的界面a> li>
<li>点击<a href="/admin/index" th:href="@{/admin/index}">去/admin/index被保护的界面a> li>
ul>
body>
html>
401.html
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<meta charset="UTF-8">
<title>401 Pagetitle>
head>
<body>
<div>
<div>
<h2>权限不够h2>
div>
<div sec:authorize="isAuthenticated()">
<p>已有用户登录p>
<p>用户:<span sec:authentication="name" />p>
<p>角色:<span sec:authentication="principal.authorities"/>p>
div>
<div sec:authorize="isAnonymous()">
<p>未有用户登录p>
div>
<p>
拒绝访问!
p>
div>
body>
html>
/user/index.html
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Hello Spring Security, User Indextitle>
<base href="/">
<link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}"/>
head>
<body>
<div th:substituteby="index::logout"/>
<h1>这个界面是被保护界面,user角色可以访问h1>
<p><a href="/index" th:href="@{/index}">返回首页a> p>
<p><a href="/admin" th:href="@{/admin/index}">去admin目录下的indexa> p>
body>
html>
/admin/index.html
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Hello Spring Security, Admin Indextitle>
<base href="/">
<link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}"/>
head>
<body>
<div th:substituteby="index::logout"/>
<h1>这个界面是被保护界面,admin角色可以访问h1>
<p><a href="/index" th:href="@{/index}">返回首页a> p>
<p><a href="/admin" th:href="@{/user/index}">去user目录下的indexa> p>
body>
html>
不登陆访问/user或者admin的界面
访问admin的界面登陆user用户(user用户只有user角色)
相反的,访问user界面,登陆admin用户(admin用户有user和admin的角色)
访问admin下的界面
然后登出
登陆user角色访问user界面
登陆失败
然后访问admin的界面
public class Student {
private String name;
private int age;
private String like;
private Student(){
}
public static Student getBuild(){
return new Student();
}
public Student name(String name){
this.name = name;
return Student.this;
}
public Student age(int age){
this.age = age;
return Student.this;
}
public Student like(String like){
this.like = like;
return Student.this;
}
public String getName(){
return this.name;
}
public int getAge(){
return this.age;
}
public String getLike(){
return this.like;
}
}
可以看到,我们在service上有两个访问,一个是获取全部的学生的getStudentList的方法,这个方法只要有任意一个权限就能够访问。而另一个方法则必须拥有ADMIN的权限的用户登录才能进行访问。
首先以USER权限进行登录:
然后获取所有的用户
然后进行尝试删除学生–小美
发现其在controller接收到请求调用服务时,因权限不够而发生异常,但是我们之前在配置时配置,当有异常出现时,自动重定向到401的界面。
所以,其展示的urlk地址是删除的地址,但是界面的内容确是,401的内容。
接下来使用admin权限的用户进行登录,然后尝试删除学生。
这里没有任何返回值,表示已经删除成功了,接下来重新获取所有的学生:
@Entity
public class Subscriber implements UserDetails, Serializable {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Column(nullable = false)
private String password;
@Column(nullable = false, unique = true)
private String username;
/*
* OneToMany 是一对多的关系,关系由多的记录的属性维护(一般情况)
* ManyToMany 是多对多的关系,关系由中间关系表维护(一般情况)
*/
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(name = "subscriber_role", joinColumns = @JoinColumn(name = "subscriber_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
/*
* 这个JoinTable的大概含义是:
* 这个中间关系由关系表维护,表名是 user_role
* 关系表有两个字段,一个是 user_id,其映射的值是user表的id
* 另一个是role_id,其映射的值是role表的id
*/
private List<Role> authorities;
public Subscriber(){
}
public Long getId(){
return id;
}
public void setId(Long id){
this.id = id;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public void setAuthorities(List<Role> authorities){
this.authorities = authorities;
}
@Override
public String getPassword() {
return password;
}
public void setPassword(String password){
this.password = password;
}
@Override
public String getUsername() {
return username;
}
public void setUsername(String username){
this.username = username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
这是启动项目前的数据库中所有的表
接着启动
中间关系表与我们猜想的一致
不过有一点没有想到,这个中间关系表竟然有外键。
用户 | 权限 |
---|---|
userA | USER |
userB | USER |
userC | USER |
adminA | ADMIN |
adminB | ADMIN |
adminC | ADMIN |
allA | USER |
allA | ADMIN |
allB | USER |
allB | ADMIN |
allC | USER |
allC | ADMIN |
我们插入上述数据:
接下来用这些用户尝试登陆,并且结合4中的逻辑,进行验证。
改动点如上图所示
登陆
userA
adminA
allA
注意点:
这里面有两个坑:
1.password需要返回加密方式:
原因:
可选
2.role返回的时候需要加前缀
原因:
使用配置的时候会自动加这个前缀,现在使用jpa则不会自动加前缀
使用Spring Security 还是比较简单的,没有想象中那么复杂。首先引入 Spring Security相关的依赖,然后写一个配置类,该配置类继承了 WebSecurityConfigurerAdapter,并在该配置类上加@EnableWebSecurity 注解开启 Web Security。再需要配置 AuthenticationManagerBuilder,AuthenticationManagerBuilder 配置了读取用户的认证信息的方式,可以从内存中读取,也可以
从数据库中读取,或者用其他的方式。其次,需要配置 HttpSecurity,HttpSecurity 配置了请求的认证规则,即哪些 URI 请求需要认证、哪些不需要,以及需要拥有什么权限才能访问。最后,如果需要开启方法级别的安全配置,需要通过在配置类上加@EnableGlobalMethodSccuriy注解开启,方法级别上的安全控制支持secureEnabled、jsr250Enabled和 prePostEnabled这3种
方式,用的最多的是prePostEnabled。其中,prePostEnabled 包括PreAuthorize和 PostAuthorize两种形式,一般只用到PreAuthorize这种方式。