1.SpringSecurity 鉴权 - [重点]
-
RBAC 基于角色访问控制
Role-Based Access Control
组成部分:
RBAC模型里面,有3个基础组成部分,分别是:用户user、角色role 和 权限permssion
User(用户):每个用户都有唯一的UID识别,并被授予不同的角色
Role(角色):不同角色具有不同的权限
Permission(权限):访问权限
用户-角色映射:用户和角色之间的映射关系
角色-权限映射:角色和权限之间的映射
-- 基于资源(权限) 细粒 用户-权限
SELECT p.* FROM rbac_perms p INNER JOIN rbac_user_perm up ON p.id=up.permid
INNER JOIN rbac_user u ON u.id=up.userid
WHERE u.username='zhangsan'
-- 基于角色 粗粒度 用户-角色-权限
SELECT p.* FROM rbac_perms p INNER JOIN rbac_role_perm rp ON p.id=rp.permid
INNER JOIN rbac_user_role ur ON rp.roleid=ur.roleid
INNER JOIN rbac_user u ON u.id=ur.userid
WHERE u.username='zhangsan'
-
配置类注解
step1:在配置类上添加@EnableWebSecurity
step2:配置类上添加@EnableGlobalMethodSecurity指定scecurity鉴权时使用的是哪一套注解
Spring Security 支持三套注解:
jsr250 注解 @DenyAll、@PermitAll、@RolesAllowed secured 注解 @Secured prePost 注解 @PreAuthorize、@PostAuthorize
实现步骤:
(1) 在配置类上添加注解配置
@EnableWebSecurity
//@EnableGlobalMethodSecurity(jsr250Enabled = true) //开启Security注解鉴权
//@EnableGlobalMethodSecurity(securedEnabled = true)
@EnableGlobalMethodSecurity(prePostEnabled = true) //sprintSecurity自带 可以支持Spring EL表达式
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
}
(2) 在控制器方法上使用注解,表示必须拥有该注解标识的权限才能访问
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/userList")
//一旦使用此注解,表示请求该方法的用户权限集里必须该权限标识符
//@RolesAllowed("ROLE_teacher:list") //访问到数据库表中的权限标识符必须以ROLE_开头,注解上的ROLE_可以省略
//@Secured("ROLE_teacher:list") //访问到数据库表中的权限标识符必须以ROLE_开头 注解上的ROLE_不能省略
//@PreAuthorize("hasAnyAuthority('teacher:list')") //使用hashAnyAuthority EL表达式,可以指定权限标识,不要求使用ROL_ 开头
//@PreAuthorize("hasAnyRole('ROLE_teacher:list')") //使用hashAnyROLE EL表达式,可以指定权限标识,要求使用ROL_ 开头 ,数据库表中的权限标识也必须以ROLE开头
@PreAuthorize("hasRole('ROLE_teacher:list')")
public List<User> queryUserList(){
return userService.list(null);
}
}
-
权限不足的处理方案
/**
* 权限不足的处理
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
ResponseResult<Void> result = ResponseResult.error(ResultCode.NO_PERMISSION);
response.setContentType("application/json;charset=utf-8");
PrintWriter out= response.getWriter();
out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
out.flush();
out.close();
}
}
配置类
http.authorizeRequests().antMatchers("/login", "/login.html")
.permitAll().anyRequest().authenticated()
.and().
// 设置登陆页、登录表单form中action的地址,也就是处理认证请求的路径
formLogin().loginPage("/login.html").loginProcessingUrl("/login")
//登录表单form中密码输入框input的name名,不修改的话默认是password
.usernameParameter("username").passwordParameter("password")
//登录认证成功后默认转跳的路径
//.defaultSuccessUrl("/home")
// 前后端分离认证成功的处理器 -输出json
.successHandler(myAuthenticationSuccessHandler)
// 前后端分离认证失败的处理器 -输出json
.failureHandler(myAuthenticationFailureHandler)
.and()
// 前后端分离处理未登录请求
.exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint)
// 前后端分离处理权限不足的请求
.accessDeniedHandler(myAccessDeniedHandler);
//.failureUrl("/error1").permitAll();
//关闭CSRF跨域攻击防御
http.csrf().disable();
2.SpringSecurity整合JWT - [重点]
2-1 JWT概述
-
有状态与无状态比较
有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份
有状态缺点是什么?
- 服务端保存大量数据,增加服务端压力
- 服务端保存用户状态,无法进行水平扩展
- 客户端请求依赖服务端,多次请求必须访问同一台服务器
服务器不需要记录客户端的状态信息:
无状态服务器优点:
- 客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同一台服务
- 服务端的集群和状态对客户端透明
- 服务端可以任意的迁移和伸缩
- 减小服务端存储压力
-
无状态登录流程
(1) 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
(2) 认证通过,将用户信息进行加密形成token字符串 [自我描述信息]
(3) 将生成token串发送到客户端
(4) 以后每次请求,客户端都携带认证的token
(5) 服务的对token进行解密,判断是否有效。
-
JWT的Token串生成
JWT全称是Json Web Token 是JSON风格轻量级的授权和身份认证规范
JWT的Token串由三部分组成
-
header 头信息 -采用base64编码生成--类型与生成时间
-
Payload 载荷 - 用户身份信息,过期时间,签发人 -采用base64编码
-
Signature 签名 是整个数据的认证信息- header+Payload+密钥 secret-RSA非对称加密技术生成
使用JWT实现服务端交互流程:
- 1、用户登录
- 2、服务的认证,通过后根据secret生成token
- 3、将生成的token返回给浏览器
- 4、用户每次请求携带token --header 通过客户端的请求头发送token串
- 5、服务端利用公钥解读jwt签名,判断签名有效后,从Payload中获取用户信息
- 6、处理请求,返回响应结果
2-2 实现生成token
-
添加依赖jar
<!--用于生成JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.1</version>
</dependency>
-
编写工具类JwtTokenUtil并测试
public class JwtTokenUtil {
/**
* 过期时间50分钟
*/
private static final long EXPIRE_TIME = 5 * 60 * 10000;
/**
* jwt 密钥
*/
private static final String SECRET = "woniuxy";
/*
生成签名 50分钟过期
*/
public static String createSign(String userName) throws Exception {
try {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(SECRET);
return JWT.create()
// 将 user id 保存到 token 里面
.withAudience(userName)
// 50分钟后token过期
.withExpiresAt(date)
//.withClaim()
//.withSubject(userName)
// token 的密钥
.sign(algorithm);
}catch(Exception ex){
ex.printStackTrace();
throw new Exception("签名错误");
}
}
/**
* 根据token获取username
* @param token
* @return
*/
public static String getUserId(String token) {
try {
String userId = JWT.decode(token).getAudience().get(0);
return userId;
} catch (JWTDecodeException e) {
throw new JWTDecodeException("生成的token 异常");
}
}
/**
* 校验token 是否有效
* @param token
* @return
*/
public static boolean checkSign(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm)
// .withClaim("username", username)
.build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (JWTVerificationException exception) {
throw new RuntimeException("token 无效,请重新获取");
}
}
public static void main(String[] args) throws Exception {
//测试生成Token串
String strToken = JwtTokenUtil.createSign("zhangsan");
System.out.println(strToken);
//eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ6aGFuZ3NhbiIsImV4cCI6MTY1NzA5MzQ3MX0.OdoENj363dPW2YVQfrc4SigoYlt45ydEtkgIc4xzzRo
//验证 token是否有效
boolean isValid = JwtTokenUtil.checkSign("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ6aGFuZ3NhbiIsImV4cCI6MTY1NzA5MzQ3MX0.OdoENj363dPW2YVQfrc4SigoYlt45ydEtkgIc4xzzRo");
System.out.println(isValid);
//从给定的token串获取用户信息
String username = JwtTokenUtil.getUserId("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ6aGFuZ3NhbiIsImV4cCI6MTY1NzA5MzQ3MX0.OdoENj363dPW2YVQfrc4SigoYlt45ydEtkgIc4xzzRo");
System.out.println(username);
}
}
2-3 SpringSecurity整合JWT-返回JWT token
第1步,登录认证成功,生成token并返回
**
* 自定义认证成功的处理器Handler
*/
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
try {
//获取当前登录认证成功的用户名
String username = request.getParameter("username");
String strToken = JwtTokenUtil.createSign(username);
//通过响应的json返回客户端
ResponseResult<String> result = new ResponseResult<>(strToken,"OK",200);
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
//将对象转json输出
out.write(new ObjectMapper().writeValueAsString(result));
out.flush();
out.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
第2步,携带Token发送请求
要想使FilterSecurityInterceptor过滤器放行:
1. Spring Security 上下文( Context ) 中要有一个 Authentication Token ,且应该是已认证状态。
2. Authentication Token 中所包含的 User 的权限信息要满足访问当前 URI 的权限要求。
实现思路:
关键在于:在 FilterSecurityInterceptor 之前 要有一个 Filter 将用户请求中携带的 JWT 转化为 Authentication Token 存在 Spring Security 上下文( Context )中给 “后面” 的 FilterSecurityInterceptor 用。
基于上述思路,我们要自定义实现一个 Filter :
/**
* 将用户请求中携带的 JWT 转化为 Authentication Token
* 存入 Spring Security 上下文( Context )
* 表示每次请求只执行该过滤器一次
*/
@Component
public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private UserMapper userMapper;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
//获取当前请求的uri
String uri = request.getRequestURI();
//如果是认证请求
if(uri.endsWith("/login")){
//放行
filterChain.doFilter(request,response);
return;
}
//不是认证请求--获取请求中的头部的token串
String strToken = request.getHeader("strToken");
if(StringUtils.isEmpty(strToken)){
//抛出自定义异常 -Token为null
myAuthenticationFailureHandler.onAuthenticationFailure(request,response,
new TokenIsNullException("Token为空!"));
return ;
}
//不是空,且不是认证请求
try {
//检验token是否有效
if (JwtTokenUtil.checkSign(strToken)) {
//获取token中的用户名
String username = JwtTokenUtil.getUserId(strToken);
//查询数据库获取用户的权限集
List<String> percodes = userMapper.getPerCodesByPerm(username);
List<GrantedAuthority> authorities = new ArrayList<>();
percodes.forEach(percode->{
authorities.add(new SimpleGrantedAuthority(percode));
});
//封装数据库存询的用户信息
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
= new UsernamePasswordAuthenticationToken(username,"",authorities);
//存入securityContext
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
//放行
filterChain.doFilter(request,response);
}
}catch(Exception e){
///抛出自定义异常 Token无效
myAuthenticationFailureHandler.onAuthenticationFailure(request,response,
new TokenIsInvalidException("Token无效!"));
}
}
}
然后将过滤器插入到FilterChainPrxoy代理的过滤器链中的UsernamePasswordAuthencationFilter前面
//将自定义的JwtTokenAuthenticationFilter插入到过滤器链中的指定的过滤器前面
http.addFilterBefore(jwtTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
3.注销成功处理方案
注销成功之后返回登录的页面,逻辑是没有错的,但是在前后端分离的情况下是返回登录页面吗?显然不是,而是返回注销成功的信息
于是,我们再去定制一个LogoutSuccessHandler
//注销成功的处理器
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
String headerToken = request.getHeader("strToken");
System.out.println("logout header Token:"+headerToken);
if(!StringUtils.isEmpty(headerToken)){ //如果token不是空
SecurityContextHolder.clearContext(); //清空上下文 用户名与权限集UsernamePasswordAuthenticationToken
ResponseResult<String> result = new ResponseResult<>("","注销成功",200);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(new ObjectMapper().writeValueAsString(result));
}else{
ResponseResult<Void> result = ResponseResult.error(ResultCode.TOKEN_IS_NULL);
response.setContentType("application/json;charset=utf-8");
PrintWriter out= response.getWriter();
out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
out.flush();
out.close();
}
}
}
WebSecurityConfig配置类中 配置注销成功处理器
// 前后端分离处理注销成功操作
.and().logout().logoutSuccessHandler(myLogoutSuccessHandler);
//关闭session最严格的策略 -JWT认证的情况下,不需要security会话参与
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);