springboot整合security
1 前言
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。提供了完善的认证机制和方法级的授权功能。是一款非常优秀的权限管理框架。它的核心是一组过滤器链,不同的功能经由不同的过滤器。这篇文章就是想通过一个小案例将Spring Security整合到SpringBoot中去。要实现的功能就是在认证服务器上登录,然后获取Token,再访问资源服务器中的资源。
2 基本概念
2.1单点登录
什么叫做单点登录呢。就是在一个多应用系统中,只要在其中一个系统上登录之后,不需要在其它系统上登录也可以访问其内容。举个例子,京东那么复杂的系统肯定不会是单体结构,必然是微服务架构,比如订单功能是一个系统,交易是一个系统…那么我在下订单的时候登录了,付钱难道还需要再登录一次吗,如果是这样,用户体验也太差了吧。实现的流程就是我在下单的时候系统发现我没登录就让我登录,登录完了之后系统返回给我一个Token,就类似于身份证的东西;然后我想去付钱的时候就把Token再传到交易系统中,然后交易系统验证一下Token就知道是谁了,就不需要再让我登录一次。
2.2JWT
上面提到的Token就是JWT(JSON Web Token),是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范。一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。为了能够直观的看到JWT的结构,我画了一张思维导图:
最终生成的JWT令牌就是下面这样,有三部分,用 . 分隔。
base64UrlEncode(JWT 头)+“.”+base64UrlEncode(载荷)+“.”+HMACSHA256(base64UrlEncode(JWT 头) + “.” + base64UrlEncode(有效载荷),密钥)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
2.3 RSA
从上面的例子中可以看出,JWT在加密解密的时候都用到了同一个密钥 “ robod666 ”,这将会带来一个弊端,如果被黑客知道了密钥的内容,那么他就可以去伪造Token了。所以为了安全,我们可以使用非对称加密算法RSA。
RSA的基本原理有两点:
私钥加密,持有私钥或公钥才可以解密
公钥加密,持有私钥才可解密
Spring Security 是针对 Spring 项目的安全框架,也是 Spring Boot 底层安全模块默认的技术选型。他可以实现强大的 web 安全控制。对于安全控制,我们仅需引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理。
应用程序的两个主要区域是“认证”和“授权”(或者访问控制),这两个主要区域是安全的两个目标。 身份验证意味着确认您自己的身份,而授权意味着授予对系统的访问权限。
认证 (你是谁,说白了就是一个用户登录的功能,帮我们验证用户名和密码)
授权 (你能干什么,就是根据当前登录用户的权限,说明你能访问哪些接口,哪些不能访问。)
认证
身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。系统确定您是否就是您所说的使用凭据。在公共和专用网络中,系统通过登录密码验证用户身份。身份验证通常通过用户名和密码完成。
授权
另一方面,授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。简单来说,授权决定了您访问系统的能力以及达到的程度。验证成功后,系统验证您的身份后,即可授权您访问系统资源。
WebSecurityConfigurerAdapter:自定义 Security 策略
AuthenticationManagerBuilder:自定义认证策略
@EnableWebSecurity:开启 WebSecurity 模式
UserDetails
因为我们最后都是需要从数据库里面读取账户信息的,所以我们需要在我们定义的用户实体类中实现UserDetails接口中的一些方法,这其中主要就是包括用户 角色,权限 那部分的信息.
UserDetailsService
其实大家看到Service就知道是什么意思了,就是需要我们将我们在对应的UserService中实现UserDetailsService的方法,主要就是实现用户的 认证授权 操作
SecurityConfig
这个一看名字就知道是配置文件了.这里主要配置 密码的加密方式,HTTP过滤 等功能
2 用户实体类实现UserDetails
这里我们点进来看一下,发现UserDetails里面主要是下面这几个属性:
Collection<? extends GrantedAuthority> getAuthorities();//用户的权限集
String getPassword();//用户的加密后的密码
String getUsername();//应用内唯一的用户名
boolean isAccountNonExpired();//账户是否过期
boolean isAccountNonLocked();//账户是否锁定
boolean isCredentialsNonExpired();//凭证是否过期
boolean isEnabled();//用户是否可用
当我们继承了UserDetails这个接口之后我们就需要重写上述的所有方法,同下面的代码:
private Collection<? extends GrantedAuthority> authorities;//这里说是权限集,其实里面存放的其实是用户的角色信息,并且是字符串的形式
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;//这里的返回类型是固定的,不能修改成你自己定义的角色对象
}
@Override
public String getUsername() {
return loginName;//这里返回你自己定义的唯一用户名的白能量名称
}
//下面四个默认都返回true就行了
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
3 用户业务层实现UserDetailsService
这里我们需要将我们的UserService继承UserDetailsService,然后去实现他里面的方法,这里我们点进源码看了之后发现,他里面就一个方法
这里我们重写一下他的这个方法
@SneakyThrows
@Override
public UserDao loadUserByUsername(String username) throws AuthenticationException {
//首先通过用户名查询是否有该用户
UserDao userDao=userDaoMapper.getUserByLoginName(username);
//System.out.println(userDao);
//如果用户存在就开始执行身份认证以及授权的相关操作
if(userDao!=null)
{
// HttpSession session = request.getSession();
// session.setAttribute("userDao",userDao);
// session.setAttribute("sessusername",username);
//这里是判断用户的状态即是否锁定
if(userDao.getState()==0)
{
throw new AccountLockedException("账户已锁定!");
}
else {
List<GrantedAuthority> authorities = new ArrayList<>();
//查询出来该用户的角色列表
List<RoleDao> roleDaoList=roleService.getRoleByUserId(userDao.getUserId());
//权限列表
List<RightDao> rightDaoList=new ArrayList<>();
for(RoleDao roleDao:roleDaoList)
{
authorities.add(new SimpleGrantedAuthority("ROLE_"+roleDao.getName()));
for(RightDao rightDao:rightService.getRightsByRoleId(roleDao.getRoleId()))
{
rightDaoList.add(rightDao);
}
}
// System.out.println(username+"用户已经登录");
// System.out.println("有以下角色");
// for(GrantedAuthority a:authorities)
// System.out.println(a);
// System.out.println("有以下权限");
// for(RightDao rightDao:rightDaoList)
// System.out.println(rightDao);
return new UserDao(username,userDao.getPassword(),authorities,rightDaoList);
}
}
else
{
throw new UsernameNotFoundException("用户名不存在!");
}
}
这里面有几个注意点
- 对于用户的角色信息,springsecurity都是将该信息存储在 authorities这个列表之中,并且当我们点进去查看里面存储的数据类型的时候我们可以发现GrantedAuthority本质上其实是一个字符串
之后我们在查看我们添加到 authorities列表中的SimpleGrantedAuthority是什么样子的
我们查看之后可以发现他的构造函数只有一个,并且构造函数的变量就是字符串形式的role,并非是我们自己定义的role对象,并且这里有一个注意点就是我们添加的role字符串必须要是这样的形式: ROLE_角色名 否则springsecurity是识别不了角色信息的.
还有一个就是我们最后的返回对象,他默认的返回对象是UserDetails
所以他默认的返回对象应该是这样的
return new UserDao(username,userDao.getPassword(),authorities);
返回一个包含用户名,用户名真实密码,角色集的User对象,但是我这里额外又添加了一个属性即权限集,是因为我后面可能会用到权限操作,所以我将它一同返回了,这个属性不是必须的.
但是如果你有额外返回的属性,那么你就需要在User对象定义一个你相应的构造函数,就如同我这样:
注意密码的返回形式,我们这里是直接返回的用户的真实密码(这里已经加密过了),否则是无法进行身份验证的环节的.
这样基本的 身份验证以及授权操作 就已经完成了
4 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</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-web</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
5 SecurityConfig
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 授权
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 首页所有人可以访问,功能页根据权限开发访问
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
// 没有访问权限跳转至登录页
http.formLogin()
.loginPage("/toLogin") // 设置登录地址
.loginProcessingUrl("/login") // 设置登录认证地址
.usernameParameter("username") // 用户名参数
.passwordParameter("password"); // 密码参数
// 关闭csrf功能
http.csrf().disable();
// 开启注销功能
http.logout()
.logoutSuccessUrl("/"); // 设置登出成功地址
// 开启记住我功能
http.rememberMe()
.rememberMeParameter("remember-me"); // 记住我参数
}
/**
* 认证
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 从内存中读取认证数据
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()) // 设置密码加密规则
.withUser("root").password(new BCryptPasswordEncoder().encode("00000000")).roles("vip1", "vip2", "vip3")
.and()
.withUser("user1").password(new BCryptPasswordEncoder().encode("00000000")).roles("vip1")
.and()
.withUser("user2").password(new BCryptPasswordEncoder().encode("00000000")).roles("vip2", "vip3");
}
}
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled=true,jsr250Enabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserService userService;
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
// 设置默认的加密方式
return new BCryptPasswordEncoder();
}
@Autowired
public void configure(AuthenticationManagerBuilder builder) throws Exception {
builder.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
/**
* 静态资源设置
*/
// @Override
// public void configure(WebSecurity webSecurity) {
// //不拦截静态资源,所有用户均可访问的资源
// webSecurity.ignoring().antMatchers(
// "/",
// "/css/**",
// "/js/**",
// "/images/**",
// "/layui/**"
// );
// }
/**
* http请求设置
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/","/login").permitAll()
.antMatchers("/admin/**").hasRole("admin")
.anyRequest().authenticated()
.and()
.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.defaultSuccessUrl("/swagger-ui.html")
.failureUrl("/login?error")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login")
.permitAll()
.and()
.httpBasic()
.disable()
.csrf()
.disable();
}
}
这里我们主要注意下面这几个点:
我们必须要将 加密规则 注入到spring容器中,否则会报错,其次就是将加密规则添加到我们的service,这样在进行身份验证的时候它才能解析我们已经加密过的密码,否则他是解析不了的,主要就是这段代码:
@Bean
public BCryptPasswordEncoder passwordEncoder() {
// 设置默认的加密方式
return new BCryptPasswordEncoder();
}
@Autowired
public void configure(AuthenticationManagerBuilder builder) throws Exception {
builder.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
定义我们的过滤条件
这里主要是在 configure(HttpSecurity http),看到参数是HTTP,我们就知道主要是对HTTP请求进行过滤,认证等操作,首先我们看第一段代码:
.authorizeRequests()
.antMatchers("/","/login").permitAll()
.antMatchers("/admin/**").hasRole("admin")
.anyRequest().authenticated()
这里我们将登陆页面定义为全部允许,之后定义/admin下的所有请求都需要拥有admin角色的用户才能够访问,之后就是所有的所有的请求都需要用户在已经登录的情况下才能够进行访问
每当一段请求规则定义完成之后,如果你想要的重新定义另一段请求规则可以通过and进行隔断,进行再一次的请求规则编写
这里我们看第二段代码:
.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.defaultSuccessUrl("/swagger-ui.html")
.failureUrl("/login?error")
.permitAll()
自定义登录页面以及错误页面
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login")
.permitAll()
配置了使用SpringSecurity的内置/login和/loginout接口(这个是完全可以自定义的)
权限被拒绝后的返回结果也可以自定义,它当权限被拒绝后,会抛出异常
使用.and()只为了表示一上配置结束,并满足链式调用的要求,不然之前的对象可能并不能进行链式调用
6 Thymeleaf 中使用 Security
// 展示登录名
<div th:text="${#authentication.name}"></div>
// 使用属性获取登录名
<div sec:authentication="name">
The value of the "name" property of the authentication object should appear here.
</div>
// 条件判断,判断是否有ADMIN这个角色
<div th:if="${#authorization.expression('hasRole(''ROLE_ADMIN'')')}">
This will only be displayed if authenticated user has role ROLE_ADMIN.
</div>
// 使用属性判断是否有相应的角色(权限)
<div sec:authorize="${hasRole(#vars.expectedRole)}">
This will only be displayed if authenticated user has a role computed by the controller.
</div>
// 获取登录用户的相应权限(角色)
<div th:text="${#authentication.getAuthorities()}"></div>
7 登录页
<!--登录注销-->
<div class="right menu">
<!--未登录-->
<div sec:authorize="${!isAuthenticated()}">
<a class="item" th:href="@{/toLogin}">
<i class="address card icon"></i> 登录
</a>
</div>
<!--已登录-->
<div sec:authorize="${isAuthenticated()}">
<a class="item">
用户名:<span th:text="${#authentication.name}"></span>  
角色:<span th:text="${#authentication.getAuthorities()}"></span>
</a>
</div>
<div sec:authorize="${isAuthenticated()}">
<!--注销-->
<a class="item" th:href="@{/logout}">
<i class="sign-out icon"></i> 注销
</a>
</div>
</div>
注意:
thymeleaf-extras-springsecurity
如果是使用4.x版本的,那么html文件中的命名空间是 xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4
如果是用的5.x的版本,那么html文件中的命名空间是 xmlns:sec=“http://www.thymeleaf.org/extras/spring-security”
总结:
仔细阅读上边的那个流程图,是理解SpringSecurity最重要的内容,代码啥的都很简单;上边也就两个类,一个配置接口与角色的关系,一个实现了UserDetailsService类中的方法。
前边说了,SpringSecurity主要就是两个逻辑:
用户登录后,将用户的角色信息保存在服务器(session中);
用户访问接口后,从session中取出用户信息,然后和配置的角色和权限进行比对是否有这个权限访问
上述方法中,我们只重写了用户登录时的逻辑。而根据访问接口来判断当前用户是否拥有这个接口的访问权限部分,我们并没有进行修改。所以这只适用于可以使用session的项目中。
对于前后端分离的项目,一般是利用JWT进行授权的,所以它的主要内容就在判断token中的信息是否有访问这个接口的权限,而并不在用户登录这一部分。
解决访问的方案有很多种,选择自己最适合自己的才是最好了。SpringSecurity只是提供了一系列的接口,他自己内部也有一些实现,你也可以直接使用。
上边配置和用户登录逻辑部分的内容是完全可以从数据库中查询出来进行配置的。