目录
1.第一个请求就是类似于前端请求一个登录SpringSecurity内置验证的安全登录中心
启动类:SpringsecurityJwtApplication 类
前言:
所有代码看最后即可 。
第一部分:代码测试:
我们测试时,采用的是postman充当客户端进行测试。
先进行启动:
分为两次请求
1.第一个请求就是类似于前端请求一个登录SpringSecurity内置验证的安全登录中心
解析如下:
为的是获取到请求服务端中我们进行配置的Controller接口的权限。如果这个登录都验证不过去,那么就没有权限去进行请求之后的
Controller中配置的接口资源
我们对于SpringSecurity内置的安全登录,是可以进行自定义配置请求地址的:我们这里配置的是/doLogin。如图:在配置类中进行配置的
我们对于SpringSecurity内置的安全登录,是可以进行自定义配置可成功登录该内置登录中心的成员用户信息,我们这里配置的是:
用户名:mxy 密码:123以及所拥有的权限集合
2.第二个请求就是在第一个请求成功的基础上进行请求的
第二个请求就是对后端服务器中的Controller类中的接口对应的资源进行的请求。
当第一个请求执行成功之后,说明SpringSecurity登录验证已经成功 。
说明可以继续请求后端资源接口。
第一个请求之后,我们就会获取一个JWT。这个JWT中就包含着我们的用户信息。然后我们就把这个JWT配置给Headers中的Authorization。
第二部分:流程分析【重点】
第一个请求的流程:
首先我们使用postman进行请求我们自定义设置的SpringSecurity安全登录验证的接口doLogin
并且配置请求url的携带参数。参数为登录的用户名以及密码 !
具体步骤:
1.无论什么请求都会先进行经过过滤器
2.但是由于这是security的登录验证请求,其中并没有token,所以我们直接通过过滤器中的解析JWT的过程。
3. 跳过过滤器之后,我们会进行验证security登录。此时我们要进行判断,postman发送的请求url
中携带的用户名以及密码参数是否在我们自定义的用户中
我们自定义的用户为 :用户名为mxy ,密码为123
4.如果登录失败:会进入到我们登录失败的处理器中进行处理
5.如果登录成功:同理会跳转到登录成功处理器进行处理操作:
登录成功处理器,最后会把用户信息进行设置给JWT中。
然后把JWT设置给access_token。并且设置过期时间,以及token的类型为bearer
设置完成之后进行使用流进行写出 其中还使用到了转化。
我们通过流写出响应给客户端浏览器的数据对应的格式为JSON,编码为Utf-8
6.结束之后,postman(客户端)上会显示服务端响应回来的数据,该数据为Json格式
这些数据就是我们上边利用PrintWriter打印流进行写出的:
第二个请求的流程:
首先第二个请求就是对后端服务器中的Controller类中的接口对应的资源进行的请求。
当第一个请求执行成功之后,说明SpringSecurity登录验证已经成功 说明可以继续请求后端资源接口。
第二个请求的前提:
(1)第一个请求成功
(2)携带第一个请求之后返回的access_token。这个access_token其实就是JWT。我们把这个JWT
设置给Authorization。格式为 :bear+空格+JWT
具体步骤:
1.这个请求首先会被过滤器进行拦截
2.过滤器发现这个用户已经成功进行了security验证登录。那么当该用户进行这一次请求的时候,
肯定会携带一个access_token。这个东西就是JWT。JWT中封装的就是我们用户的信息 !
所以说:过滤器的作用就是继续解析这个JWT。
3.解析完成之后,再进行封装设置。
对两次请求的总结:
当第一次请求时,客户端postman携带想要进行登录的用户信息,服务端接收请求,然后进行登录
验证。登录验证成功之后,也就是第一次请求之后,服务端想要写出响应给客户端浏览器用户信息
(即是JWT),但是服务端会把JWT封装叫做access_token进行真正响应给客户端。
所以其实JWT=access_token。
第一次请求之后,客户端具有了已经登录用户的信息 即是access_token。
当第二次请求发出时,客户端postman把第一次请求获取到access_token中JWT数据进行设置给
第二次请求的Header字段中Authorization。第二次请求进行发出,此时过滤器进行拦截到然后进行
对这个Authorization字段进行解析。解析得到的是用户信息,此时对于第二次请求到达服务端而言
我们已经具有了该用户的信息。我们可以通过该用户的信息进行检验是否该用户已经完成了
security登录验证。如果完成了,那么就可以接着进行下一步对后端服务器资源的请求。所以说,
当第二次请求完成了过滤器对其JWT的解析之后,后端服务器就已经获取到了这个用户对应的信
息。所以当我们进行分为后端服务器资源的时候,我们会进行权限判断,此时由于我们第二次请求
中的过滤之后,我们已经把解析之后的JWT的数据 即是用户信息的数据设置给了
所以说,我们就可以进行判断是否该服务端Controller的接口是否具有用户权限进行访问
为什么要这样做?
因为在微服务的项目中,我们可以是多个服务端。所以说,我们一个用户信息保证完成一次
security登录验证之后,我们就可以在所有服务端进行请求资源的权力。
第三部分:编码
配置类:WebSecurityConfig类
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtTokenCheckFilter jwtTokenCheckFilter ;
@Override
protected void configure(HttpSecurity http) throws Exception {
//所有请求发送给服务端都会先执行过滤器中的操作
http.addFilterBefore(jwtTokenCheckFilter, UsernamePasswordAuthenticationFilter.class) ;
//去掉防护
http.csrf().disable() ;
//表单提交
http.formLogin()
.loginProcessingUrl("/doLogin")
.successHandler(authenticationSuccessHandler()) //登录成功处理器
.failureHandler(authenticationFailureHandler())//登录失败处理器
.permitAll() ; //放行
//禁用session 因为我们使用的是前后端分离。根本不需要session,只要你具有token(jwt)就可以进行访问
http.sessionManagement().disable() ;
//放行所有的接口 因为我们使用了jwt ;所以只要请求携带了jwt 只要能被解析,那么就相当于登录成功!
http.authorizeRequests().antMatchers("/**").permitAll() ;//所以我们对所有请求进行放行 不做安全登录验证
}
//进行配置用户信息
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
//创建增加用户mxy
.withUser("mxy")
//设置密码 并且进行加密
.password(passwordEncoder().encode("123"))
//对用户进行设置访问权限
.authorities("sys:add","sys:query") ;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder() ;
}
/**
* 登录成功时的返回值
* 1.拿到用户信息
* 2.根据用户信息生成jwt
* 3.写出去给客户端浏览器
* @return
*/
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
//设置编码以及数据格式类型为json
response.setContentType("application/json;charaset=utf-8");
//获取到用户信息 进行强转
User principal = (User) authentication.getPrincipal();
//拿到我们的用户名
String username = principal.getUsername() ;
//拿到我们的权限对应的集合
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
List<String> authStrs = new ArrayList<>(authorities.size() * 2);//使用2倍的大小 防止轻易扩容
//遍历authStrs集合 进行设置权限
authorities.forEach(new Consumer<GrantedAuthority>() {
@Override
public void accept(GrantedAuthority author) {
//该接口重写的实现方法 内置了功能
//这个方法的最终作用效果为:
// 遍历authorities集合之后 把集合中的每一个元素的权限都设置给authStrs 这个List集合中
authStrs.add(author.getAuthority());
}
});
//生成jwt jwt其实就是我们所说的token
//1.颁发token的时间
Date createTime = new Date() ;
//2.token过期的时间
Calendar now = Calendar.getInstance();
now.set(Calendar.SECOND,7200);//设置过期时间
Date expireTime = now.getTime();//获取到过期时间expireTime
//3.token中的header
HashMap<String, Object> header = new HashMap<>(4);
header.put("alg","HS256") ;
header.put("typ","JWT") ;
//4.token的载体
String jwt = com.auth0.jwt.JWT.create()
.withHeader(header)
.withIssuedAt(createTime)
.withExpiresAt (expireTime)
//自定义的进行设置一些信息到JWT中
.withClaim("username",username)//设置用户名
.withClaim("auths",authStrs) //设置用户对应的权限
//5.设置签名
.sign(Algorithm.HMAC256(JwtConstant.SIGN)) ;
//生成JWT之后,我们就要将其写出去给客户端浏览器
HashMap<String,Object> map = new HashMap<>(8) ;
//jwt其实就是我们所说的token
map.put("access_token",jwt) ;
map.put("expire_time",JwtConstant.EXPIRE_TIME) ;
map.put("type","bearer") ;//记住:token就一种类型 那就是bearer
ObjectMapper objectMapper = new ObjectMapper();
String s = objectMapper.writeValueAsString(map) ;
PrintWriter writer = response.getWriter() ;
writer.write(s) ;
writer.flush();
writer.close();
}
} ;
}
//登录失败时
@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
//设置编码格式以及数据的类型为Json格式
response.setContentType("application/json;charset=utf-8");
//失败的话,那么就写出去给客户端浏览器即可
HashMap<String, Object> map = new HashMap<>(4);
map.put("code",401) ;
map.put("msg","登录失败") ;
ObjectMapper objectMapper = new ObjectMapper() ;
//把Map集合类型的数据转化为String类型的数据
String s = objectMapper.writeValueAsString(map) ;
PrintWriter writer = response.getWriter() ;
writer.write(s) ;
writer.flush();
writer.close();
}
} ;
}
}
常量类:JwtConstant 类
public interface JwtConstant {
/**
* 过期时间
*/
Integer EXPIRE_TIME = 7200 ;
/**
* 加密的字符串
*/
String SIGN = "security-jwt-sign" ;
/**
* header头里面的信息
*/
String AUTHORIZATION = "Authorization" ;
}
过滤器:JwtTokenCheckFilter类
@Configuration
public class JwtTokenCheckFilter extends OncePerRequestFilter {
/**
* 每次请求都会走这个方法
* jwt会从Headers带回来
* 解析jwt 然后把解析后的数据设置到上下文去
* 每一个请求都会进行解析,会大大的增大登录服务器的负担。所以说:jwt的性能没有session好
*/
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
//设置一个响应编码
response.setContentType("application/json;charset=utf-8");
//1.拿到url
String path = request.getRequestURI() ;
String method = request.getMethod() ;//拿到请求的类型 : Get Post Put Delete
if ("/doLogin".equals(path) && "POST".equalsIgnoreCase(method)) {
//直接进行放行,因为登录没有token
filterChain.doFilter(request,response) ;
return;
}
//验证jwt 先进行获取到 请求中携带的Headers字段中的authorization数据
//该数据的格式为:bearer+空格+jwt
String authorization = request.getHeader(JwtConstant.AUTHORIZATION) ;
//如果authorization有文本 即是有内容的意思
if (StringUtils.hasText(authorization)) {
//把authorization中的无用前缀 bearer+空格给进行删除 剩余的就是jwt的内容
String jwt = authorization.replaceAll("bearer ","") ;
//解析jwt
//第一步:先进行验证签名 验证完签名之后才可以进行解析
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(JwtConstant.SIGN)).build() ;
//jwt解析之后的类型为 DecodedJWT
DecodedJWT decodedJWT = null ;
try {
decodedJWT = jwtVerifier.verify(jwt) ;
} catch (JWTVerificationException e) {
//报错了,写一个401异常
response.getWriter().write("token验证失败");
return;
}
//拿到解析之后的jwt了 即是对应的用户信息的数据。我们给后端服务器进行设置一下这些数据
//拿到解析后数据中的用户名username的数据:
Claim usernameClaim = decodedJWT.getClaim("username") ;
String username = usernameClaim.asString(); //再开一层化为String类型
//拿到jwt中的权限数据
Claim authsClaim = decodedJWT.getClaim("auths") ;
List<String> authStrs = authsClaim.asList(String.class); //String.class 表示转化为的集合中的元素类型为String
//转变权限信息 容量设置为2倍避免轻易就扩容
ArrayList<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>(authStrs.size() * 2);
authStrs.forEach(new Consumer<String>() {
//该接口对象中重写方法的作用就是:
//把authStrs集合中的每一个String类型的元素进行遍历
// 然后转变为SimpleGrantedAuthority类型的元素设置到simpleGrantedAuthorities集合中
@Override
public void accept(String authStr) {
simpleGrantedAuthorities.add(new SimpleGrantedAuthority(authStr)) ;
}
});
//变成Security认识的对象
UsernamePasswordAuthenticationToken authenticationToken =
//该token参数为:用户名 密码和权限集合列表
new UsernamePasswordAuthenticationToken(username,null,simpleGrantedAuthorities) ;
// 怎么才能让security认识呢?
//void setAuthentication(Authentication var1);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//代码继续往下执行 放行
filterChain.doFilter(request,response) ;
}
}
}
启动类:SpringsecurityJwtApplication 类
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringsecurityJwtApplication {
public static void main(String[] args) {
SpringApplication.run(SpringsecurityJwtApplication.class, args);
}
}
测试时的接口:TestController类
@RestController
public class TestController {
@GetMapping("test1")
@PreAuthorize("hasAuthority('sys:add')")
public String test1() {
return "add成功" ;
}
@GetMapping("test2")
@PreAuthorize("hasAuthority('sys:delete')")
public String test2() {
return "delete成功" ;
}
}