下期文章整合Jwt+Security
二、创建jwt_secueiry工程
1 、创建工程完毕之后
2、修改启动类 新增注解
3、修改配置文件 yml
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://yby6.com:3310/psringsecurity?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: xxxx password: xxxx redis: host: xxxx port: xxxx database: xxxx password: xxxx mybatis: mapper-locations: classpath:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
4、使用idea连接数据库,进行代码生成
可以看到除了persistent_logins 其它的就是RBAC表了, 如果不懂RBAC的小伙伴的话请自行百度学习即可 so easy to happy
右击生成User表CRUD
5、创建完毕,开始配置SpringSecurity 的配置啦。
1. 创建WebSecurityConfig配置类
package top.yangbuyi.config; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import top.yangbuyi.constant.JwtConstant; import java.io.PrintWriter; import java.time.Duration; import java.util.*; /** * ClassName: WebSecurityConfig * * @author yangshuai * @Date: 2021-04-09 14:42 * @Description: http请求配置 $ **/ @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { /** * 权限403返回json */ @Autowired private AccessDecision accessDecision; /** * redis */ @Autowired private RedisTemplate<String, String> redisTemplate; /** * http请求 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { // 关闭csrf攻击 http.csrf().disable(); // 登录配置 http.formLogin() .loginProcessingUrl("/doLogin") // 指定登录地址 .successHandler(authenticationSuccessHandler()) // 登录成功执行的 .failureHandler(authenticationFailureHandler()) // 登录失败执行的 .permitAll(); // 403 权限不足 http.exceptionHandling().accessDeniedHandler(accessDecision); // 基于token方式 不会存储session来进行登录判断了 http.sessionManagement().disable(); // 集成JWT 全部放行接口 使用jwt过滤器来进行 鉴权 http.authorizeRequests().antMatchers("/**").permitAll(); } /** * 登录成功的 handle * * @return */ @Bean public AuthenticationSuccessHandler authenticationSuccessHandler() { return (request,response,authentication) -> { System.out.println("登录成功了"); // 把json串写出去 response.setContentType("application/json;charset=utf-8"); HashMap<String, Object> map = new HashMap<>(8); map.put("code", 200); map.put("msg", "登录成功"); // 把用户信息返回给前端 让前端可以保存起来 map.put("data", authentication); ObjectMapper objectMapper = new ObjectMapper(); String s = objectMapper.writeValueAsString(map); // 写出去 PrintWriter writer = response.getWriter(); writer.write(s); // 刷新流 关闭流 writer.flush(); writer.close(); }; } /** * 登录失败的json * * @return */ @Bean public AuthenticationFailureHandler authenticationFailureHandler() { return (request, response, exception) -> { 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(); String s = objectMapper.writeValueAsString(map); PrintWriter writer = response.getWriter(); writer.write(s); writer.flush(); writer.close(); }; } }
新增jwt依赖
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.11.0</version> </dependency>
6.自定义配置用户信息(数据库当中获取)
1.修改user实体类
@Data @AllArgsConstructor @NoArgsConstructor public class sysUser implements Serializable, UserDetails { private Integer userid; private String username; private String userpwd; private String sex; private String address; private List<String> authors = Collections.emptyList(); private static final long serialVersionUID = 1L; /** * 返回查询出来的权限给到springSecurity * * @return the authorities, sorted by natural key (never <code>null</code>) */ @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<SimpleGrantedAuthority> authorityArrayList = new ArrayList<>(); if (this.authors.size() > 0) { authors.forEach(e -> authorityArrayList.add(new SimpleGrantedAuthority(e))); } return authorityArrayList; } /** * Returns the password used to authenticate the user. * * @return the password */ @Override public String getPassword() { return this.userpwd; } /** * Indicates whether the user's account has expired. An expired account cannot be * authenticated. * * @return <code>true</code> if the user's account is valid (ie non-expired), * <code>false</code> if no longer valid (ie expired) */ @Override public boolean isAccountNonExpired() { return true; } /** * Indicates whether the user is locked or unlocked. A locked user cannot be * authenticated. * * @return <code>true</code> if the user is not locked, <code>false</code> otherwise */ @Override public boolean isAccountNonLocked() { return true; } /** * Indicates whether the user's credentials (password) has expired. Expired * credentials prevent authentication. * * @return <code>true</code> if the user's credentials are valid (ie non-expired), * <code>false</code> if no longer valid (ie expired) */ @Override public boolean isCredentialsNonExpired() { return true; } /** * Indicates whether the user is enabled or disabled. A disabled user cannot be * authenticated. * * @return <code>true</code> if the user is enabled, <code>false</code> otherwise */ @Override public boolean isEnabled() { return true; } }
2.创建UserDetailsServiceImpl 到config配置下
import org.springframework.beans.factory.annotation.Autowired; 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.Component; import top.yangbuyi.domain.sysUser; import top.yangbuyi.mapper.sysUserMapper; import java.util.List; /** * ClassName: UserDetailsServiceImpl * * @author yangshuai * @Date: 2021-04-09 14:55 * @Description: 用户信息配置 $ **/ @Component public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private sysUserMapper sysUserMapper; /** * 登录时会来到这里进行 用户校验 相当于shiro当中的 认证用户真实性 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 根据用户名称查询用户信息 sysUser sysUser = sysUserMapper.selectName(username); if (sysUser != null){ List<String> a = sysUserMapper.selectAuthor(sysUser.getUserid()); System.out.println("权限标识符:{}"+ a); sysUser.setAuthors(a); } return sysUser; } }
3.修改mapper
@Select(" select * from sys_user where username = #{username} ") sysUser selectName(String username); @Select(" select distinct sp.percode from sys_user_role sur left join sys_role_permission srp on sur.roleid = srp.roleid left join sys_permission sp on srp.perid = sp.perid where sur.userid = #{userid} ") List<String> selectAuthor(Integer userid);
4.修改WebSecurityConfig配置文件
4.启动项目测试登录
zhangsan/123456
相当于复习了上面的知识点只是将内存用户改为自己数据库当中的用户
芜湖!!!报错了,咱们看到控制台打印 咦熟悉~~~~
加密器没有注入欸,在SpringSeccurity当中是需要加密器来进行登录的
来设置设置。。。
修改web配置文件
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
恭喜恭喜 登录成功了
7.咦不对呀 不是要玩jwt嘛 ,不急 这就开始…
思考:
1. jwt用来干啥啊????
哎,搞这么久还不知道? 用来搞权限啊 如果用户没有进行登录 直接访问我们的业务 那肯定不行的呀 那么带着这些问题 我们冲冲冲!!!
2. 登录时我们如何接入jwt呢?
在SpringSecurity登录成功时会执行successHandel里面的自定义方法,我们在里面把用户信息存储到JWT然后呢
这时我们就需要使用到过滤器了,在springsecurity执行登录前 我们自己来进行校验登录 即可
看我操作…
1. 登录成功后我们进行将登录成功的用户数据存储到JWT 修改登录成功的handel 用于jwt颁发token
/** * 登录成功的 handle * 1. 拿到用户的信息 * 2. 组装JWT * 3. 写出去 * * @return */ @Bean public AuthenticationSuccessHandler authenticationSuccessHandler() { return (request,response,authentication) -> { // 1.拿去用户数据 UserDetails principal = (UserDetails) authentication.getPrincipal(); String username = principal.getUsername(); // 2.拿起权限 Collection<? extends GrantedAuthority> authorities = principal.getAuthorities(); List<String> authorizations = new ArrayList<>(authorities.size() * 2); authorities.forEach(e -> authorizations.add(e.getAuthority())); // 3.组装jwt // 生成jwt // 颁发时间 Date createTime = new Date(); // 过期时间 Calendar now = Calendar.getInstance(); // 设置未来的时间 now.set(Calendar.MINUTE, JwtConstant.EXPIRETIME); Date expireTime = now.getTime(); // header HashMap<String, Object> header = new HashMap<>(4); header.put("alg", "HS256"); header.put("typ", "JWT"); // 设置载体 String sign = JWT.create() .withHeader(header) .withIssuedAt(createTime) .withExpiresAt(expireTime) .withClaim("username", username) .withClaim("authorizations", authorizations) .sign(Algorithm.HMAC256(JwtConstant.SIGN)); ObjectMapper objectMapper = new ObjectMapper(); HashMap<String, Object> map = new HashMap<>(4); map.put("access_token", sign); map.put("expire_time", JwtConstant.EXPIRETIME); map.put("type", "bearer"); // 保存redis --- 客户端和服务器来建立有状态 redisTemplate.opsForValue().set(JwtConstant.JWT_KET + sign, sign, Duration.ofMinutes(JwtConstant.EXPIRETIME)); String s = objectMapper.writeValueAsString(map); PrintWriter writer = response.getWriter(); writer.write(s); writer.flush(); writer.close(); }; }
2.创建JwtCheckToken类 用户校验用户来的token是否合法并且是否由我颁发的
import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import top.yangbuyi.constant.JwtConstant; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * ClassName: JwtCheckFiter * * @author yangshuai * @Date: 2021-03-29 09:56 * @Description: jwt鉴权 $ **/ @Component public class JwtCheckFilter extends OncePerRequestFilter { @Autowired private StringRedisTemplate stringRedisTemplate; /** * 每一次请求都会进行执行改过滤器 * 每次请求都会走这个方法 * jwt 从header带过来 * 解析jwt * 设置到上下文当中去 * jwt 性能没有session好 * * @param httpServletRequest * @param httpServletResponse * @param filterChain * @throws ServletException * @throws IOException */ @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { httpServletResponse.setContentType("application/json;charset=utf-8"); String requestURI = httpServletRequest.getRequestURI(); // 获取请求 url 来判断是否是登录 String method = httpServletRequest.getMethod(); // 请求类型 if ("/doLogin".equals(requestURI) && "POST".equalsIgnoreCase(method)) { filterChain.doFilter(httpServletRequest, httpServletResponse); return; } // 判断是否带入了token String header = httpServletRequest.getHeader(JwtConstant.AUTHORIZATION); if (StringUtils.hasText(header)) { // 解析真正的 token header = header.replaceAll(JwtConstant.JWTTYPE, ""); // 判断token是否存在 Boolean aBoolean = stringRedisTemplate.hasKey(JwtConstant.JWT_KET + header); // 校验是否过期 登出 if (!aBoolean) { // 说明已经登出或者过期 httpServletResponse.getWriter().write("token过期,请重新登录!"); return; } // 解析验证 JWTVerifier build = JWT.require(Algorithm.HMAC256(JwtConstant.SIGN)).build(); DecodedJWT verify = null; try { verify = build.verify(header); } catch (JWTVerificationException e) { httpServletResponse.getWriter().write("token验证失败"); return; } // 拿到解析后的jwt了 后端服务器没保存用户信息 给他设置进去 Claim usernameClaim = verify.getClaim("username"); String username = usernameClaim.asString(); // 拿到权限 Claim authsClaim = verify.getClaim("authorizations"); List<String> authStrs = authsClaim.asList(String.class); // 转变权限信息 ArrayList<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>(authStrs.size() * 2); authStrs.forEach(auth -> simpleGrantedAuthorities.add(new SimpleGrantedAuthority(auth))); // 变成security认识的对象` // 参数一: 用户名称 // 参数二:密码 // 参数三:权限标识 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, simpleGrantedAuthorities); SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 重新存储到Security当中 filterChain.doFilter(httpServletRequest, httpServletResponse); // 放行 return; } httpServletResponse.getWriter().write("token验证失败."); return; } }
3.修改web配置文件
将自定义的过滤器放在springSecurity执行登录之前调用,用于校验token 的合法权限
4.重新启动测试token
登录成功,返回token
根据token带入 调用业务接口
好像忘记写业务接口了 不过没关系 补上不就行了
5.创建IndexController
/** * ClassName: IndexController * * @author yangshuai * @Date: 2021-04-09 16:11 * @Description: 业务 $ **/ @RestController public class IndexController { @RequestMapping("query") @PreAuthorize("hasAuthority('sys:query')") public String query() { return "query"; } @RequestMapping("add") @PreAuthorize("hasAuthority('sys:add')") public String add() { return "add"; } @RequestMapping("delete") @PreAuthorize("hasAuthority('sys:delete')") public String delete() { return "delete"; } @RequestMapping("update") @PreAuthorize("hasAuthority('sys:update')") public String update() { return "update"; } }
重新启动测试token
可以带入刚刚我们的登录过的token 因为我们设置了过期时间 2小时嘛
欸 访问成功 我们试一试 没有权限的
8.JWT登出
思考:
1.咋记录登录的唯一性呀
思路:
在用户登录成功后进入到handel我们进行记录当前jwt颁发的token到redis当中
然后就是,访问登出接口 我们根据传递来的token前往redis当中查询,判断是否存在 存在删除 即可。
这就引发出了我们自定义的 过滤器来校验 上次登录的token判断redis当中是否存在 就判断为 过期或者登出.
这段代码功能我已经写在Demo里面的IndexController了 小伙伴们课自己研究查看。
@Autowired private StringRedisTemplate stringRedisTemplate; @PostMapping("toLogout") public String toLogout() { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); String header = request.getHeader(JwtConstant.AUTHORIZATION); String jwtToken = header.replaceAll(JwtConstant.JWTTYPE, ""); if (StringUtils.hasText(jwtToken)) { // 移除token stringRedisTemplate.delete(JwtConstant.JWT_KET + jwtToken); } return "退出成功!"; }