1.前言
观看这篇随笔需要有spring security基础。
心得: 1.生成token 的变化数据是用户名和权限拼接的字符串 ,其他的固定 2.生成的token是将登录通过的用户的权限拼接的字符串加密后放入里面后加密,当携带token访问时被拦截后,会将token解析出的权限注册,因为不与数据库等数据共享校验权限最新信息, 如果在携带token的请求前权限有变化,但是token却没有改变,会导致token权限与用户真实权限不一致,形成脏数据啦!!! 如果权限增加还好,使得无法访问新加权限的操作,如果是减少权限,比如vip过期,用户仍然可以有vip权限。 3.解决token脏数据的方案有两个: (1)等待该token失效时间【不靠谱】; (2)每次修改权限时,会强制使得token失效,具体怎么做,还没试过 4.当然,也有优点的,不与数据库等最新数据做权限对比操作,较少了访问数据库该用户信息的部分,能快速的过滤请求权限,理论上访问数据会变快。 5.可以设置过期时间,单位毫秒,用时间戳设置 ,到时间则不可在使用, 但是缺点很明显,在未过期之前,可以无数次访问验证通过,无法控制使用次数, 因此不能作为资源服务器对第三方应用开放的授权令牌, 6.令牌格式对不上,会直接报错异常,为了服务降级,做个异常捕获即可 7.如果生成了新的令牌,旧的令牌仍然可以使用,因此会导致多设备同时登录的情况,无法控制登录数量 8.使用jwt[java web token],做登录校验,则会导致http.sessionManagement().maximumSessions(1);设置失效,因为没有使用session做为登录控制 // 安全弊端很多 , 但是让我深刻明白了token的内部思想
完全可以使用redis来完成用户token的管理,但是这样每次都需要向redis查询一次,就让jave web token 不伦不类了 。。。。
虽然已经尽可能的让服务器减少负担和提高反应速度,但仍然感觉好鸡肋
//
当然应用场景还是有的,可用于分享连接的使用,这样既能向有令牌的用户使用被分享的权限,而且不需要去数据库获取数据对其进行对比,降低服务器负担,
也就是说比较适合半开放性的功能使用
//
工程我放在GitHub仓库了
https://github.com/cen-xi/spring-security-JWT
2.操作
看我的源码大招,写了很多注释了,足够详细了,我懒得再写说明
(1)目录结构
(2)添加依赖包
pom.xml源码
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.0.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>security-jwt-5605</artifactId> <version>0.0.1-SNAPSHOT</version> <name>security-jwt-5605</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <!-- jwt依赖--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
(3)做一个实体类,继承 UserDetails
package com.example.securityjwt5605.model; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.List; public class JwtUser implements UserDetails { //属性名 username 和 password 是固定死的,不可更改,否则报错 //但 grantedAuthorities 可随意 private String username; private String password; private List<GrantedAuthority> grantedAuthorities; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return grantedAuthorities; } @Override public String getPassword() { return password; } @Override public String getUsername() { return 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; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public void setGrantedAuthorities(List<GrantedAuthority> grantedAuthorities) { this.grantedAuthorities = grantedAuthorities; } }
(4)用户名密码登录过滤器
package com.example.securityjwt5605.filters; import com.example.securityjwt5605.model.JwtUser; import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * 首次登录才调用这个方法 */ public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter { //构造方法 ,记得使用 public //第一个是 登录路径 。第二个是 认证管理者 //在启动的时候就已经h已经执行了 public JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) { super(new AntPathRequestMatcher(defaultFilterProcessesUrl)); System.out.println("===============================登录拦截1=================================="); //存储到父类,可不加 super.便于方法 attemptAuthentication()调用, setAuthenticationManager(authenticationManager); } /** *访问/login登录后首先进入这里 */ @Override public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse resp) throws AuthenticationException, IOException, ServletException { JwtUser user = new JwtUser(); System.out.println("===============================登录拦截2=================================="); try { //从请求中获取用户验证信息 //将json字符串解析 user = new ObjectMapper().readValue(req.getInputStream(), JwtUser.class); // }catch (Exception ignored){ // //Exception ignored表示忽略异常 // //这样内部可以不写内容 // } // String username = req.getParameter("username"); // String password = req.getParameter("password"); // if (username == null || password == null){ // throw new Exception(); // } // user.setUsername(username); // user.setPassword(password); }catch (Exception e){ //Exception ignored表示忽略异常 System.out.println("请求无法解析出JwtUser对象"); } //对请求做认证操作,如何校验,由默认的程序完成,不涉及对比操作,因为用户信息存在内存中,否则需要修改 securityConfig.java 的 configure(AuthenticationManagerBuilder auth) 用于设置数据库操作 //认证管理对象执行认证方法,new 一个用户密码认证令牌对象,参数为用户名和密码,然后放入认证方法中 //然后执行登录验证 return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword())); } //认证成功 @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { System.out.println("===============================登录拦截3=================================="); //获取登录角色的权限 //这是权限 ,如果登录内存只有角色配置,无权限配置,则自动添加前缀构成权限 ROLE_角色 Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities(); //线程安全 StringBuffer stringBuffer = new StringBuffer(); for (GrantedAuthority grantedAuthority : authorities) { System.out.println("当前有的权限:"+grantedAuthority); //用逗号隔开好一点,不然后面需要手动切割 stringBuffer.append(grantedAuthority.getAuthority()).append(","); } //生成令牌 token String jwt = Jwts.builder() //登录角色的权限,这会导致如果权限更改,该token无法及时更新权限信息 .claim("authorities", stringBuffer) //用户名 .setSubject(authResult.getName()) //存活时间,过期则判为无效 .setExpiration(new Date(System.currentTimeMillis() + 1000 * 10)) //签名,第一个参数时算法,第二个参数时内容,内容可随意写 .signWith(SignatureAlgorithm.HS512, "java521@java") //协议完成 .compact(); System.out.println(jwt); System.out.println("======================"); System.out.println(stringBuffer); //设置json数据返回给前端 Map<String, Object> map = new HashMap<>(); map.put("token", jwt); map.put("msg", "登录成功"); //MediaType.APPLICATION_JSON_UTF8_VALUE 等用于 "application/json;charset=UTF-8" // response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.setContentType("application/json;charset=utf-8"); PrintWriter printWriter = response.getWriter(); //转成json后传送 printWriter.write(new ObjectMapper().writeValueAsString(map)); //关闭流 printWriter.flush(); printWriter.close(); } //认证失败 @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { System.out.println("===============================登录拦截4=================================="); Map<String, Object> map = new HashMap<>(); map.put("msg", "登录失败"); response.setContentType("application/json;charset=utf-8"); PrintWriter printWriter = response.getWriter(); //转成json后传送 printWriter.write(new ObjectMapper().writeValueAsString(map)); //关闭流 printWriter.flush(); printWriter.close(); } }
(5)token访问过滤器
package com.example.securityjwt5605.filters; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.web.filter.GenericFilterBean; import sun.plugin.liveconnect.SecurityContextHelper; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.security.Security; import java.util.List; /** * 对携带token的请求做token检查,对比是否正确,正确则可以直接通过 */ public class JwtFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("===============================token登录拦截1=================================="); //强转http请求 HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; //从请求头获取数据 //定死了名称为 authorization String tokenStr = httpServletRequest.getHeader("authorization"); System.out.println(tokenStr); /* 打印结果 【不可换行,这里为了展示才换行】 Bearer eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfYWRtaW4sIiwic3ViIjoiYWRtaW4iLCJleHAi OjE1OTE2MzAwMTF9.oHmTl-f5RetmFJ8rM5MaIruOkA83sqt-6F7f2c27QRWdJvTAOIYX_VbRCngodaROZ4jprQ1ktwz5sZAWcDJdkg */ System.out.println("=========================================="); if (tokenStr != null) { System.out.println("有认证令牌"); boolean k = true; Jws<Claims> jws = null; try { //解析,解析方式使用加密时配置的数字签名对应 //一旦令牌修改成位数对比不上,会报错。。。 jws = Jwts.parser().setSigningKey("java521@java") .parseClaimsJws(tokenStr.replace("Bearer", "")); System.out.println(tokenStr.replace("Bearer", "")); /* 打印结果 【不可换行,这里为了展示才换行】 eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfYWRtaW4sIiwic3ViIjoiYWRtaW4iLCJleHAiOjE 1OTE2MzAwMTF9.oHmTl-f5RetmFJ8rM5MaIruOkA83sqt-6F7f2c27QRWdJvTAOIYX_VbRCngodaROZ4jprQ1ktwz5sZAWcDJdkg */ } catch (Exception e) { //放令牌被修改、时间过期,都会抛出异常,由方法 parseClaimsJws()安抛出的异常 // e.printStackTrace(); k = false; } if (k) { // 令牌解析成功 Claims claims = jws.getBody(); //获取token解析出来的用户名 String username = claims.getSubject(); System.out.println(username); /* 打印结果 [ROLE_admin,ROLE_user,等等] */ //从token获取登录角色的权限 //如果时以逗号格式配置字符串,可用以下方式解析,否则手动解析 List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities")); System.out.println(grantedAuthorities); // //new令牌登录校验 对象,参数分别是 : 用户名 ,盐[没有则设为null] ,角色/权限 UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, grantedAuthorities); //执行令牌登录校验 SecurityContextHolder.getContext().setAuthentication(token); } else { System.out.println("令牌解析失败,被修改了"); SecurityContextHolder.getContext() .setAuthentication(new UsernamePasswordAuthenticationToken(null, null, null)); } } else { System.out.println("没有认证令牌"); SecurityContextHolder.getContext() .setAuthentication(new UsernamePasswordAuthenticationToken(null, null, null)); } System.out.println("//让过滤器继续往下走,"); //让过滤器继续往下走 filterChain.doFilter(servletRequest, servletResponse); } } /* 总结: 1.生成token 的变化数据是用户名和权限拼接的字符串 ,其他的固定 2.生成的token是将登录通过的用户的权限拼接的字符串加密后放入里面后加密,当携带token访问时被拦截后,会将token解析出的权限注册,因为不与数据库等数据共享校验权限最新信息, 如果在携带token的请求前权限有变化,但是token却没有改变,会导致token权限与用户真实权限不一致,形成脏数据啦!!! 如果权限增加还好,使得无法访问新加权限的操作,如果是减少权限,比如vip过期,用户仍然可以有vip权限。 3.解决token脏数据的方案有两个: (1)等待该token失效时间【不靠谱】; (2)每次修改权限时,会强制使得token失效,具体怎么做,还没试过 4.当然,也有优点的,不与数据库等最新数据做权限对比操作,较少了访问数据库该用户信息的部分,能快速的过滤请求权限,理论上访问数据会变快。 5.可以设置过期时间,单位毫秒,用时间戳设置 ,到时间则不可在使用, 但是缺点很明显,在未过期之前,可以无数次访问验证通过,无法控制使用次数, 因此不能作为资源服务器对第三方应用开放的授权令牌, 6.令牌格式对不上,会直接报错异常,为了服务降级,做个异常捕获即可 7.如果生成了新的令牌,旧的令牌仍然可以使用,因此会导致多设备同时登录的情况,无法控制登录数量 8.使用jwt[java web token],做登录校验,则会导致http.sessionManagement().maximumSessions(1);设置失效,因为没有使用session做为登录控制 // 安全弊端很多 , 但是让我深刻明白了token的内部思想 */
(6)服务层根据用户名获取用户信息【为了简便,没有使用数据库,直接赋值】
package com.example.securityjwt5605.filters; import com.example.securityjwt5605.model.JwtUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; /** * 这个类其实就是为了获取用户的正确认证信息,不做信息比较,比较是在过滤器里面, * 名字叫做 UsernamePasswordAuthenticationFilter */ @Service public class MyUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { JwtUser tUser = new JwtUser(); //权限设置 List<GrantedAuthority> simpleGrantedAuthorities = new ArrayList<>(); System.out.println("===============================数据库层对比=================================="); if (username.equals("cen")) { tUser.setUsername(username); tUser.setPassword("$2a$10$Qghi7vHdyQJHYlAO.FCo/u3gCbqwWBVaSHjIF0Vci.C5.1l71SExq"); simpleGrantedAuthorities.add(new SimpleGrantedAuthority("ROLE_user")); tUser.setGrantedAuthorities(simpleGrantedAuthorities); } else if (username.equals("admin")) { tUser.setUsername(username); tUser.setPassword("$2a$10$ywq3gn6E15tnY3URptsIz.zn/fznWGqc2VhO4zphS/sIbWZJtLCVK"); simpleGrantedAuthorities.add(new SimpleGrantedAuthority("ROLE_admin")); tUser.setGrantedAuthorities(simpleGrantedAuthorities); } else { throw new UsernameNotFoundException("没有找到用户"); } // System.out.println("============================="); // //根据用户名去数据库查询用户信息 // TUser tUser = userService.getByUsername(username); // if (tUser == null){ // throw new UsernameNotFoundException("用户不存在!"); // } // //权限设置 // List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>(); // String role = tUser.getRole(); // //分割权限名称,如 user,admin // String[] roles = role.split(","); // System.out.println("============================="); // System.out.println("注册该账户权限"); // for (String r :roles){ // System.out.println(r); // //添加权限 // simpleGrantedAuthorities.add(new SimpleGrantedAuthority("ROLE_"+r)); //// simpleGrantedAuthorities.add(new SimpleGrantedAuthority(r)); // } // tUser.setGrantedAuthorities(simpleGrantedAuthorities); // System.out.println("============================="); /** * 创建一个用于认证的用户对象,包括:用户名,密码,权限 * */ //输入参数 // return new org.springframework.security.core.userdetails.User(tUser.getUsername(), tUser.getPassword(), simpleGrantedAuthorities); // 这个返回值的类型,继承了userdetails即可 return tUser; } }
(7)contoller接口
package com.example.securityjwt5605.controller; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; @RestController public class HHController { //开启跨域 // [普通跨域] //@CrossOrigin //[spring security 跨域] @CrossOrigin(allowCredentials = "true", allowedHeaders = "*") @RequestMapping("/hello") public Map<String, Object> hello() { Map<String, Object> map = new HashMap<>(); map.put("data", "hello"); return map; } //开启跨域 // [普通跨域] //@CrossOrigin //[spring security 跨域] @CrossOrigin(allowCredentials = "true", allowedHeaders = "*") @RequestMapping("/admin") public Map<String, Object> admin() { Map<String, Object> map = new HashMap<>(); map.put("data", "i am admin"); return map; } }
(8)security配置类
package com.example.securityjwt5605.config; import com.example.securityjwt5605.filters.JwtFilter; import com.example.securityjwt5605.filters.JwtLoginFilter; import com.example.securityjwt5605.filters.MyUserDetailsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.lang.reflect.Method; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyUserDetailsService myUserDetailsService; /** * 全局的跨域配置 */ @Bean public WebMvcConfigurer WebMvcConfigurer() { return new WebMvcConfigurer() { public void addCorsMappings(CorsRegistry corsRegistry) { //仅仅让/login可以跨域 corsRegistry.addMapping("/login").allowCredentials(true).allowedHeaders("*"); //仅仅让/logout可以跨域 corsRegistry.addMapping("/logout").allowCredentials(true).allowedHeaders("*"); //允许所有接口可以跨域访问 //corsRegistry.addMapping("/**").allowCredentials(true).allowedHeaders("*"); } }; } /** * 忽略过滤的静态文件路径 */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring() .antMatchers( "/js/**/*.js", "/css/**/*.css", "/img/**", "/html/**/*.html" ); } //内存放入可登录的用户信息 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { System.out.println("===============================认证管理构造器=================================="); //直接注册信息到内存,会导致jrebel热更新失效,无法更新该内容 // //如果仅仅设置了roles,则权限自动设置并自动添加前缀 为 ROLE_【角色内部的字符串,可以设置多个】, //字符串不可再添加ROLE_,会报java.lang.IllegalArgumentException: ROLE_user cannot start with ROLE_ (it is automatically added) //意思是用 ROLE_前缀会自动添加, // auth.inMemoryAuthentication().withUser("cen") // .password("$2a$10$Qghi7vHdyQJHYlAO.FCo/u3gCbqwWBVaSHjIF0Vci.C5.1l71SExq").roles("user") // //如果使用了roles 和 authorities ,那么roles将失效,将会注册authorities内部的字符串为权限,且不会添加前缀名ROLE_ // .and().withUser("admin") // .password("$2a$10$ywq3gn6E15tnY3URptsIz.zn/fznWGqc2VhO4zphS/sIbWZJtLCVK").roles("user").authorities("ROLE_admin"); // // //因此用户cen的权限为ROLE_user //用户admin的权限为 admin // // //调用数据库层,根据用户名获取用户信息回来, auth.userDetailsService(myUserDetailsService) //设置加密方式 .passwordEncoder(passwordEncoder()); } @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } //过滤规则,一旦设置了重写了这个方法,必须设置登录配置 //在启动的时候就执行了 @Override protected void configure(HttpSecurity http) throws Exception { System.out.println("===============================过滤规则=================================="); http.authorizeRequests() .antMatchers("/hello").hasRole("user") .antMatchers("/admin").hasRole("admin") // .antMatchers("/admin").hasAuthority("admin") //当访问/login的请求方式是post才允许通过 .antMatchers(HttpMethod.POST, "/login").permitAll() // .anyRequest() .anyRequest().authenticated() .and() //首次登录拦截。仅允许post访问/login .addFilterBefore(new JwtLoginFilter("/login", authenticationManager()), UsernamePasswordAuthenticationFilter.class) //token验证拦截 .addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class) // .cors() .and() .csrf().disable(); // //使用jwt[java web token],做登录校验,则该设置失效,因为没有使用session做为登录控制 // http.sessionManagement().maximumSessions(1); } }
(9)同时做了简易的前端访问页面【前后端分离,前端端口是5601 ,后端是5605】
jwt.html
<!DOCTYPE html> <html lang="zh" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta charset="UTF-8"> <title>[JWT] 测试</title> </head> <body> jave web token [JWT] 测试 <hr> <div> <label> 账户: <input id="name" type="text"> </label> <label> 密码: <input id="psw" type="text"> </label> </div> <button onclick="dosome(1)">登录</button> <hr> <hr> <button onclick="dosome(3)">登出</button> <hr> <label> token: <input id="url" type="text" value="http://localhost:5605/hello"> <span id="token"></span> </label> <button onclick="dotoken()">点我token访问</button> <hr> 返回的结果:<span id="res"></span> <!--当前路径是/html/** ,因此需要返回一级 ,所以用 ../js/ --> <script src="../js/jquery-1.11.1.min.js"></script> <script src="../js/base64.js"></script> <script> //当前最新的token let token = ""; function dotoken() { let url = ""+($("#url").val()).trim(); if (url == ""){ console.log("url不可空") return; } $.ajax({ //请求头添加token //方法一: // beforeSend: function (request) { // request.setRequestHeader("Authorization", token); // }, //方法二: headers: { //认证信息 Authorization: token }, async: true, type: 'post', dataType: "json", url: url, xhrFields: {withCredentials: true}, //前端适配:允许session跨域 crossDomain: true, success: function (data) { console.log(data); //请求成功回调函数 if (data != null) { // alert("有数据返回") $("#res").html(JSON.stringify(data)) } else { alert("系统异常") } }, error: function (xhr, type, errorThrown) { //异常处理; console.log("异常处理") console.log(JSON.stringify(xhr)); if (xhr.readyState == 4 && xhr.status == 403){ $("#res").html("403无权访问") } /* {"readyState":4,"responseText":"{"timestamp":"2020-06-08T16:51:15.016+00:00", "status":403,"error":"Forbidden","message":"","path":"/admin"}", "responseJSON":{"timestamp":"2020-06-08T16:51:15.016+00:00","status":403,"error":"Forbidden", "message":"","path":"/admin"},"status":403,"statusText":"error"} */ console.log(type); console.log(errorThrown); } }); } function dosome(type) { let name = ""; let psw = ""; let url = ""; if (type == 1) { name = ($("#name").val()).trim(); psw = ($("#psw").val()).trim(); //登录 url = "http://localhost:5605/login"; } else if (type == 3) { //登出 url = "http://localhost:5605/logout"; } //URL是URI的子集,所有的URL都是URI,但不是每个URI都是URL,还有可能是URN。 $.ajax({ async: true, type: 'post', //对应于后端 parama 方式获取数据 ,使用req.getParameter获取 // data: {"username": name, "password": psw}, //对应于后端raw方式获取数据,需要json解析,使用req.getInputStream()获取 data: JSON.stringify({"username": name, "password": psw}), //这里类型是json,那么跨域的后端需要是map类型、po实体类等 json类型 才能接收数据 dataType: "json", url: url, xhrFields: {withCredentials: true}, //前端适配:允许session跨域 crossDomain: true, // //请求头设置 // headers: { // //认证信息 // Authorization: authorization // }, success: function (data) { console.log(data); //请求成功回调函数 if (data != null) { // alert("有数据返回") $("#res").html(JSON.stringify(data)) token = data.token; $("#token").html(token); } else { alert("系统异常") } }, error: function (xhr, type, errorThrown) { //异常处理; console.log("异常处理") console.log(JSON.stringify(xhr)); console.log(type); console.log(errorThrown); } }); } </script> </body> </html>
3.测试
token时间设长一点,我这里设为1小时
(1)使用postman f访问 http://localhost:5605/login 进行登录
登录成功
获取令牌,访问 http://localhost:5605/hello
提交后
成功
因为用户 cen我设置了只有权限。
再次访问 http://localhost:5605/admin
无权限403被拒绝了
事实上,当令牌过期后再访问,也会抛出403结果
换一个账户
访问 http://localhost:5605/admin ,是可以访问的
(2)测试前端 跨域 访问
测试密码错误
测试登录成功
点击token访问
token 成功
换一个没有访问hello权限的账号
然后再次点击token访问
4.过滤器的先后操作
(1)工程启动,控制台打印
(2)用户名密码登录后,控制台打印
(3)携带token访问
有效的令牌
无效的令牌
奇怪的是 携带无效令牌时 会执行两次token访问过滤器,原因还不清楚
-------------------------
参考博文原址:
https://mp.weixin.qq.com/s/Sn59dxwtsEWoj2wdynQuRQ