Security背景
企业中数据是最重要的资源,对于这些数据而言,有些可以直接匿名访问,有些只能登录以后才能访问,还有一些你登录成功以后,权限不够也不能访问.总之这些规则都是保护系统资源不被破坏的一种手段.几乎每个系统中都需要这样的措施对数据(资源)进行保护.我们通常会通过软件技术对这样业务进行具体的设计和实现.早期没有统一的标准,每个系统都有自己独立的设计实现,但是对于这个业务又是一个共性,后续市场上就基于共享做了具体的落地实现,
例如:Spring的Security | Apache的shiro诞生了
Security概念
Spring Security 是一个企业级安全框架,由spring官方推出,
它对软件系统中的认证,授权,加密等功能进行封装,
在springboot技术推出以后,配置方面做了很大的简化.
市场上现在的分布式架构下的安全控制逐步的都在转向使用Spring Security
Security基本架构
不难看出,Security几乎都是过滤器,其中:
- 绿色部分为认证过滤器,需要我们自己配置,也可以配置多个认证过滤器.也可以使用Spring Security提供的默认认证过滤器
- 黄色部分为授权过滤器.Spring Security就是通过这些过滤器然后调用相关对象一起完成认证和授权操作
Security认证的授权分析
Security认证的登录底层调用逻辑图
Security依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Security入门
- 只需添加好依赖,即可实现网站的安全登录
- 在项目启动时,会自动生成了一个密码在控制台
- 打开浏览器输入http://localhost:8080,然后呈现登录页面
- 默认账号为user,密码则是项目启动时打印在控制台的密码
- 登录进去即可,访问该网站的所有资源
密码例如:Using generated security password: 360123aa-df93-4cd9-bab4-5212af421d2c
配置自定义 登录的账号&密码(初级)
一. 明文账号密码配置
1.在application.properties文件中配置
spring.security.user.name=user
spring.security.user.password=1234
2.配置完成后重启项目,即可使用配置的账号密码,即可实现登录
二. 加密账号密码配置
1.首先需要生成一个加密后密码
1.在Security中有一个专门加密的类:BCryptPasswordEncoder
2.通过该类的encode(""),即可生成bcrypt(加密方式)的密码
注意: 相同的字符串在bcrypt加密后的密码不是一样的
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String newPwd = encoder.encode("123456");
2.将用户和密码在springboot工程的配置文件中进行配置
’{bcrypt}加密后密码’:
- 在配置中{bcrypt}指定了密码加密时使用的算法
- 配置时需要注意password的前后必须带单引号
spring.security.user.name=user
spring.security.user.password='{bcrypt}$2a$10$fahHJIe3SJm3KcyiPPQ2d.a2qR029gB3qKHrKanQ87u.KbtZ6Phr.'
3.配置完成后重启项目,即可使用普通加密前的账号密码,即可实现登录
★ 说明:MD5和bcrypt加密的对比
MD5加密:
MD5加密方式特点:
- 一般加密都是将需要加密的内容加盐进行不可逆hash加密、
- md5算法对相同内容加密时都是相同的,并且不可逆
- md5算法对不同内容加密可能会出现相同结果,但几率很小
- md5的密文是32位的
实际运用
- 实际项目中md5盐要存储在数据库,当登录时会基于用户名,将用户信息查询出来,
- 基于账号属于的密码和数据库查询出盐进行hash md5加密,再与数据库存储的密码进行比对
- 比对结果正确,则允许登录
bcrypt加密:
1.bcrypt是对密码的一种加密方式,基于随机盐的形式进行hash不可逆加密(密文60位长度)
2.因为是随机的,所以每次生成的加密的内容也不一样,相对会更安全(密文60位长度)
3.bcrypt的密文是60位的
security实现自定义登陆逻辑
通过配置定义的缺点
SpringSecurity支持通过配置文件的方式定义用户信息(账号密码和角色等),但这种方式有明显的缺点,那就是系统上线后,用户信息的变更比较麻烦
解决策略
因此SpringSecurity也支持通过实现UserDetailsService接口的方式来提供用户认证授权信息
当写完以下配置类后,配置文件可以删除关于Security的配置
1. 创建配置类
@Configuration
public class SecurityConfig{
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
作用:
将Security框架密码加密对象(BCryptPasswordEncoder),告诉IOC容器进行管理
当security需要加密内容时,可以通过IOC取到该对象
2. 实现Security框架的UserDetailsService接口
UserDetailService 接口:
- 为SpringSecurity官方提供的登录逻辑处理对象,
- 我们自己可以实现此接口,
- 然后在对应的方法中进行登录逻辑的编写
该类返回类型UserDetails接口:
- 这里的User对象时Security内置的继承UserDetails接口
- 该类返回User对象会交给SpringSecurity框架,框架提取出密码信息,然后与用户输入的密码进行匹配校验.
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//模拟查询数据库的User表,得到的数据,如果有则继续执行; 如果没有该账号则抛出异常
if (!"abc".equals(username)) throw new UsernameNotFoundException("账号不存在");
//将用户传来的密码进行加密
String encodedPwd = passwordEncoder.encode("123456");
//创建一个Security的权限对象,模拟数据库查到的权限
List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin,ROLE_normal,sys:res:retrieve,sys:res:create");
//该对象时Security提供的UserDetails接口的实现,用于封装用户信息,也可以基于需要自己构建UserDetails接口的实现
//传入该对象的信息(账号,加密后的密码,和用户权限),将对象返回出去
//该方法返回值会交给springsecurity去校验
return new User(username,encodedPwd,grantedAuthorities);
}
}
说明 1. 当我们执行登录操作时,底层会通过过滤器等对象,调用这个方法
username : 这个参数为页面输入的用户名(账号)
User对象 : 一般是从数据库基于用户名查询到的用户信息(该对象必须实现UserDetails接口)
权限对象: 如果权限的方式是以角色分配,编写字符串时用"ROLE_"做前缀
throws: 当该方法抛出UsernameNotFoundException异常时,则账号密码校验失败
UserDetailsService:
该类必须实现该接口,重写此方法时底层才会进行调用,该接口实现定义用户登录的逻辑和权限,对该方法返回值进行检查账号和密码,控制是否允许登录、分配权限;
注意: 需要把该类加入IOC容器
添加权限字符串语法
ROLE_admin: ROLE_是关键字,必须大写,用于设定该角色权限。后面权限可以自定义
sys:res:retrie: 自定义系统资源权限,可以按照自己需求写入权限格式
3.测试运行
当完成以上两步即可完成Security登录页面账号与密码,动态的登录访问服务器资源
权限这块,后续会详细更新
实现自定义登陆页面进行登录
1.配置放行的登录页面,及登录资源、登录失败,登录成功页面
推荐可在之前security的配置类进行页面配置
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)//告诉系统底层在启动时,进行访问权限的初始化配置
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();//关闭跨域攻击,用于前后端分离的项目
http.formLogin()//获取表单登录对象
.loginPage("/login.html")//配置登录使用的页面(也可以是其他域名的页面)
//设置前端发起登录请求的url地址,请求方式必须为post请求
.loginProcessingUrl("/login")
.usernameParameter("uname")//设置后端接收的账号参数名称,不设置时默认为username
.passwordParameter("pass")//设置后端接收的密码参数名称,不设置时默认为password
//设置登录成功跳转的Url地址(重定向)
//登录失败默认跳转的是:/login.html?error
.defaultSuccessUrl("http://www.lili.con")
.failureUrl("http://www.baidu.com")//设置登录失败默认跳转的页面(重定向)
//设置登录成功服务器内部转发到那个contorller接口进行业务处理(可以执行自定义功能,参数)
//注意不可以直接填写地址,否则报405异常
.successForwardUrl("/index.html")
.successHandler(new RedirectAuthenticationSuccessHandler("跳转的网址"))//自定义登录成功处理器(可返回json)
.failureHandler(new RedirectAuthenticationFailureSuccessHandler());//自定义登录失败处理器(可返回json)
//获取表单注销登录对象
http.logout()
.logoutUrl("/logout")//设置注销时请求的地址
.logoutSuccessUrl("/login.html");//设置访问成功跳转到那里
//获取处理异常的对象
http.exceptionHandling()
//设置当用户没有登录时,security执行的处理器
.authenticationEntryPoint(new DefaultAuthenticationEntryPoint())
//设置角色没有权限时,security执行的处理器
.accessDeniedHandler(new DefaultAccessDeniedException());
//该对象的功能是放行指定的资源,或页面,不需要认证就可以访问指定的资源
http.authorizeRequests()//拿到设置请求的资源授权的对象
//放行需要放行的资源,放行images下所有的资源包括子目录下所有的内容,可以指定多个
//允许匿名访问写上面,需要权限访问写下面
.antMatchers("/login.html", "/images/**")
.permitAll()//允许上面配置直接访问
.anyRequest().authenticated();//除了以上资源,必须认证才可访问
}
}
什么是转发?
转发又叫服务器内部转发,用户请求的接口收到消息后,该接口将直接取访问服务器内部的接口进行处理后,在将数据返回给前端请求的接口,由该接口把数据返回给前端
匹配资源授权的对象时
* 用于匹配0个或多个字符
** 用于匹配0个或多个目录及字符
@EnableGlobalMethodSecurity(prePostEnabled = true)
注解是由SpringSecurity提供(启用全局方法权限访问的安全性)
用于权限配置类,告诉系统底层在启动时,进行访问权限的初始化
注意:
1.以上表单登录对象设置的方法不是必须都写,开发时选择适合自己的设置即可;
2.该类必须实现WebSecurityConfigurerAdapter的接口
3.实现该接口的方法,需要把super.configure(http);代码删除
实现自定义Security 的各种业务处理器
说明:
现在的很多系统都采用的是前后端分离设计,我们登陆成功以后可能会跳转到前端系统的某个地址,或者返回一个json数据,我们可以自己定义登录成功的处理操作,例如
重点说明:
定义以下异常处理器后,全局异常不可捕获包含以下的异常,否则直接会被全局异常捕获而跳过以下处理器
1.自定义登录失败处理器
//实现自定义登录失败处理器
public class RedirectAuthenticationFailureSuccessHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
}
}
2.自定义登录成功处理器
自定义登录成功处理器必须实现AuthenticationSuccessHandler接口
//实现自定义登录成功处理器
public class RedirectAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private String redirectUrl;
public RedirectAuthenticationSuccessHandler(String redirectUrl) {
this.redirectUrl = redirectUrl;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.sendRedirect(redirectUrl);
}
}
3.自定义用户权限的异常处理器(处理没有认证的访问异常)
public class DefaultAccessDeniedException implements AccessDeniedHandler {
//此方法用于处理AccessDeniedException对象
//@param exception: 访问被拒绝的的异常对象
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException exception) throws IOException, ServletException {
}
}
4.自定义未认证(未登录)异常的处理器(处理没有认证的访问异常)
/**
* 假如用户没有认证,就去访问需要认证才可以访问的资源,系统
* 底层会抛出一个异常AuthenticationException,系统默认
* 对此异常的处理方式是跳转到登录页面,假如现在我们不是要
* 跳转到登录页面,而是要返回一个json格式的字符串,则需要
* 自己定义AuthenticationEntryPoint接口的实现类
*/
public class DefaultAuthenticationEntryPoint implements AuthenticationEntryPoint {
// 当系统出现AuthenticationException异常时,会自动调用此方法(commence-开始,着手)
// @param exception AuthenticationException异常通知
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException exception) throws IOException, ServletException {
}
}
对于SpringSecurity框架而言,在实现认证和授权业务时,可能出现如下两大类型异常:
- AuthenticationException (用户还没有认证就去访问某个需要认证才可访问的方法时,可能出现的异常,这个异常通常对应的状态码401)
- AccessDeniedException (用户认证以后,在访问一些没有权限的资源时,可能会出现的异常,这个异常通常对应的状态吗为403)
实现用户授权逻辑
1.确保Security 的配置类被@EnableGlobalMethodSecurity(prePostEnabled = true)
注解
2.在java项目中controller层可以对每个接口设定权限,以下是代码的实现
@RestController
public class ResourceController {
@PreAuthorize("hasRole('admin')")//设置该接口的角色权限的访问
@RequestMapping("/doCreate")
public String doCreate() {
return "数据添加成功";
}
@RequestMapping("/doCreate")
@PreAuthorize("hasAuthority('sys:res:create')")//设置该接口系统资源权限的访问
public String doCreate() {
return "数据添加成功";
}
}
@PreAuthorize(“hasRole(‘admin’)”):
注解描述方法时: 用于告诉系统访问此方法时需要进行权限检测,需要具备指定权限才可以访问
如果没有则默认是响应前端403
hasRole(‘admin’) :
1.表示用户必须有ROLE_admin权限才可以访问此接口
2.hasRole:标志着该接口配置了角色访问权限 (检查用户的合法性)
2.接口定义权限时可直接写权限名,给对象赋予角色权限时前面必须加ROLE_跟上自定义角色权限名
hasAuthority(‘sys:res:create’)
1.表示用户必须有sys:res:create权限才可以访问此接口
2.hasAuthority: 标志着该接口配置了资源访问权限 (用户登录后的权限控制)
3.定义资源名时与给予用户的权限一致即可,可以自定义权限名
注意
如果contorller不加@PreAuthorize注解,只需要验证即可访问
如果contorller 加上@ PreAuthorize注解,如果用户有该权限才能访问
Security 对用户信息的存储
- 在用户登录系统时,后端Security框架调用Session技术,使服务端创建一个储存回话状态的Session对象;
- 当Session对象创建时还会创建一个回话Cookie对象,通过这个回话Cookie将SessionId写到客户端
- 客户端下次访问服务端会携带这个回话Cookie,并通过Cookie的JsessionId找到Session对象,进而获取Session对象中存储的数据
Security 总结知识点
1.为什么要使用Security (产生背景)
判定用户身份的合法性
2.如何理解认证?(判定用户身份的合法性)
用户密码,指纹,刷脸,刷身份证
3.如何进行身份认证?
自己写认证逻辑,借助框架去写认证逻辑-尊重框架规则
4.市场上的认证和授权框架有哪些?
SpringSecurity和Shiro框架
5.为什么会选择SpringSecurity?
功能强大,SpringBoot诞生后在配置方面做了大量的简化
6.SpringSecurity中的加密方式你用的什么?
Bcrypt,底层基于随机盐方式对密码进行hash不可逆加密,更加安全,缺陷是慢
7.为什么要进行权限控制?
防止非法用户破坏数据
8.SpringSecurity进行权限控制的步骤
通过两个注解@EnableGlobalMethodSecurity,@PreAuthorize
9.用户的信息存在哪里?用户登录成功以后,如何获取我们登录的用户信息?
后端存: session(xxxxx,user)
前端存: cookie(sessionid,xxxxx)获取用户信息原理: 以cookie的值存储session的key,当用户登陆成功访问服务器时,服务器读取用户cookie的value,通过value查session的K获取到用户信息
拦截器+实现Security通过Token添加用户信息
//令牌拦截器,拦截客户端向服务器请求时传递的令牌
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.从请求中获取token对象(如何获取取决于你传递token的方式:header,params)
String token=request.getHeader("token");
//2.验证token是否存在
if(token== null|| "".equals(token)) throw new RuntimeException("请先登录");//WebUtils.writeJsonToClient
//3.验证token是否过期
if(JwtUtils.isTokenExpired(token)) throw new RuntimeException("登录超时,请重新登录");
//4.解析token中的认证和权限信息(一般存储在jwt格式中的负载部分)
Claims claims= JwtUtils.getClaimsFromToken(token);
List<String> list=(List<String>) claims.get("authorities");//这个名字应该与创建token时,指定的权限名相同
//5.封装和存储认证和权限信息
//5.1构建UserDetail对象(用户身份的象征-类似于一张名片,微信的二维码)
UserDetails userDetails= User.builder()
.username((String)claims.get("username"))
.password("")
.authorities(list.toArray(new String[]{}))
.build();
//5.2构建Security权限交互对象(记住,固定写法)
PreAuthenticatedAuthenticationToken authToken=
new PreAuthenticatedAuthenticationToken(
userDetails,//用户身份
userDetails.getPassword(),
userDetails.getAuthorities());
//5.3将权限交互对象与当前请求进行绑定
authToken.setDetails(new WebAuthenticationDetails(request));
//5.4.将认证后的token存储到Security上下文(会话对象)
SecurityContextHolder.getContext().setAuthentication(authToken);
return true;
}
}