写点什么

SpringSecurity+JWT 实现前后端分离的使用

用户头像
4ye
关注
发布于: 3 小时前
SpringSecurity+JWT实现前后端分离的使用

创建一个配置类 SecurityConfig 继承 WebSecurityConfigurerAdapter


package top.ryzeyang.demo.common.config;
import org.springframework.context.annotation.Bean;import org.springframework.security.access.hierarchicalroles.RoleHierarchy;import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;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.config.http.SessionCreationPolicy;import org.springframework.security.core.userdetails.User;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.crypto.factory.PasswordEncoderFactories;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.provisioning.InMemoryUserDetailsManager;import org.springframework.security.web.AuthenticationEntryPoint;import org.springframework.security.web.access.AccessDeniedHandler;import org.springframework.security.web.authentication.AuthenticationFailureHandler;import org.springframework.security.web.authentication.AuthenticationSuccessHandler;import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;import top.ryzeyang.demo.common.filter.JwtAuthenticationTokenFilter;import top.ryzeyang.demo.utils.JwtTokenUtil;

@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter {
final AuthenticationFailureHandler authenticationFailureHandler; final AuthenticationSuccessHandler authenticationSuccessHandler; final AuthenticationEntryPoint authenticationEntryPoint; final AccessDeniedHandler accessDeniedHandler; final LogoutSuccessHandler logoutSuccessHandler;
public SecurityConfig(AuthenticationFailureHandler authenticationFailureHandler, AuthenticationSuccessHandler authenticationSuccessHandler, AuthenticationEntryPoint authenticationEntryPoint, AccessDeniedHandler accessDeniedHandler, LogoutSuccessHandler logoutSuccessHandler) { this.authenticationFailureHandler = authenticationFailureHandler; this.authenticationSuccessHandler = authenticationSuccessHandler; this.authenticationEntryPoint = authenticationEntryPoint; this.accessDeniedHandler = accessDeniedHandler; this.logoutSuccessHandler = logoutSuccessHandler; }
@Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); }
@Bean("users") public UserDetailsService users() { UserDetails user = User.builder() .username("user") .password("{bcrypt}$2a$10$1.NSMxlOyMgJHxOi8CWwxuU83G0/HItXxRoBO4QWZMTDp0tzPbCf.") .roles("USER")// roles 和 authorities 不能并存// .authorities(new String[]{"system:user:query", "system:user:edit", "system:user:delete"}) .build(); UserDetails admin = User.builder() .username("admin") .password("{bcrypt}$2a$10$1.NSMxlOyMgJHxOi8CWwxuU83G0/HItXxRoBO4QWZMTDp0tzPbCf.")// .roles("USER", "ADMIN") .roles("ADMIN")// .authorities("system:user:create") .build(); return new InMemoryUserDetailsManager(user, admin); }
/** * 角色继承: * 让Admin角色拥有User的角色的权限 * @return */ @Bean public RoleHierarchy roleHierarchy() { RoleHierarchyImpl result = new RoleHierarchyImpl(); result.setHierarchy("ROLE_ADMIN > ROLE_USER"); return result; }
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(users()); }
@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/js/**", "/css/**", "/images/**"); }
@Override protected void configure(HttpSecurity http) throws Exception { // 自定义异常处理 http.exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler)
// 权限 .and() .authorizeRequests() // 一个是必须待身份信息但是不校验权限。 .antMatchers("/", "/mock/login").permitAll() //只允许匿名访问 .antMatchers("/hello").anonymous() .anyRequest() .authenticated()
// 表单登录// .and()// .formLogin()// .successHandler(authenticationSuccessHandler)// .failureHandler(authenticationFailureHandler)// .loginProcessingUrl("/login")// .permitAll()
// 注销 .and() .logout() .logoutUrl("/logout") .logoutSuccessHandler(logoutSuccessHandler) .permitAll()
// 关闭csrf 会在页面中生成一个csrf_token .and() .csrf() .disable()
// 基于token,所以不需要session .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS)// 添加我们的JWT过滤器 .and() .addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class) ; }
@Bean public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){ return new JwtAuthenticationTokenFilter(); }
@Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
@Bean public JwtTokenUtil jwtTokenUtil() { return new JwtTokenUtil(); }}
复制代码

创建 JWT 过滤器 继承 OncePerRequestFilter


这里直接用的 macro 大佬 mall 商城里的例子


package top.ryzeyang.demo.common.filter;
import lombok.extern.slf4j.Slf4j;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.beans.factory.annotation.Value;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;import org.springframework.stereotype.Component;import org.springframework.web.filter.OncePerRequestFilter;import top.ryzeyang.demo.utils.JwtTokenUtil;
import javax.servlet.FilterChain;import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;
/** * JWT登录授权过滤器 * * @author macro * @date 2018/4/26 */@Slf4jpublic class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Qualifier("users") @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Value("${jwt.tokenHeader}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String authHeader = request.getHeader(this.tokenHeader); if (authHeader != null && authHeader.startsWith(this.tokenHead)) { String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer " String username = jwtTokenUtil.getUserNameFromToken(authToken); log.info("checking username:{}", username); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); log.info("authenticated user:{}", username); SecurityContextHolder.getContext().setAuthentication(authentication); } } } chain.doFilter(request, response); }}
复制代码

自定义 handler

这里主要介绍两个 hanler,一个权限不足,一个验证失败的

权限不足 实现 AccessDeniedHandler


package top.ryzeyang.demo.common.handler;
import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.security.access.AccessDeniedException;import org.springframework.security.web.access.AccessDeniedHandler;import org.springframework.stereotype.Component;import top.ryzeyang.demo.common.api.CommonResult;import top.ryzeyang.demo.common.api.ResultEnum;
import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.PrintWriter;

@Componentpublic class MyAccessDeineHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter writer = httpServletResponse.getWriter(); writer.write(new ObjectMapper().writeValueAsString(new CommonResult<>(ResultEnum.ACCESS_ERROR, e.getMessage()))); writer.flush(); writer.close(); }}
复制代码

认证失败 实现 AuthenticationEntryPoint


如账号密码错误等验证不通过时


package top.ryzeyang.demo.common.handler;
import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.security.core.AuthenticationException;import org.springframework.security.web.AuthenticationEntryPoint;import org.springframework.stereotype.Component;import top.ryzeyang.demo.common.api.CommonResult;import top.ryzeyang.demo.common.api.ResultEnum;
import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.PrintWriter;

@Componentpublic class MyAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter writer = httpServletResponse.getWriter();// 认证失败 writer.write(new ObjectMapper().writeValueAsString(new CommonResult<>(ResultEnum.AUTHENTICATION_ERROR, e.getMessage()))); writer.flush(); writer.close(); }}
复制代码

JWT 工具类


这里直接用的 macro 大佬 mall 商城里的例子


稍微改了一点,因为 JDK11 用的 jjwt 版本不一样 ,语法也有些不同

pom 文件中引入 jjwt


<dependency>    <groupId>io.jsonwebtoken</groupId>    <artifactId>jjwt-api</artifactId>    <version>0.11.2</version></dependency><dependency>    <groupId>io.jsonwebtoken</groupId>    <artifactId>jjwt-impl</artifactId>    <version>0.11.2</version>    <scope>runtime</scope></dependency><dependency>    <groupId>io.jsonwebtoken</groupId>    <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->    <version>0.11.2</version>    <scope>runtime</scope></dependency>
复制代码

JwtTokenUtil

package top.ryzeyang.demo.utils;
import cn.hutool.core.date.DateUtil;import cn.hutool.core.util.StrUtil;import io.jsonwebtoken.Claims;import io.jsonwebtoken.Jwts;import io.jsonwebtoken.io.Decoders;import io.jsonwebtoken.security.Keys;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Value;import org.springframework.security.core.userdetails.UserDetails;
import javax.crypto.SecretKey;import java.util.Date;import java.util.HashMap;import java.util.Map;
/** * JwtToken生成的工具类 * JWT token的格式:header.payload.signature * header的格式(算法、token的类型): * {"alg": "HS512","typ": "JWT"} * payload的格式(用户名、创建时间、生成时间): * {"sub":"wang","created":1489079981393,"exp":1489684781} * signature的生成算法: * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret) * * @author macro * @date 2018/4/26 */@Slf4jpublic class JwtTokenUtil { private static final String CLAIM_KEY_USERNAME = "sub"; private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.secret}") private String secret;
@Value("${jwt.expiration}") private Long expiration;
@Value("${jwt.tokenHead}") private String tokenHead;
private SecretKey getSecretKey() { byte[] encodeKey = Decoders.BASE64.decode(secret); return Keys.hmacShaKeyFor(encodeKey); }
/** * 根据负责生成JWT的token */ private String generateToken(Map<String, Object> claims) { SecretKey secretKey = getSecretKey(); return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(secretKey) .compact(); }
/** * 测试生成的token * @param claims * @return */ public String generateToken2(Map<String, Object> claims) { SecretKey secretKey = getSecretKey(); return Jwts.builder() .setClaims(claims) .setIssuer("Java4ye") .setExpiration(new Date(System.currentTimeMillis() + 1 * 1000)) .signWith(secretKey) .compact(); }
/** * 从token中获取JWT中的负载 */ private Claims getClaimsFromToken(String token) { SecretKey secretKey = getSecretKey(); Claims claims = null; try { claims = Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(token) .getBody(); } catch (Exception e) { log.info("JWT格式验证失败:{}", token); } return claims; }
/** * 生成token的过期时间 */ private Date generateExpirationDate() { return new Date(System.currentTimeMillis() + expiration * 1000); }
/** * 从token中获取登录用户名 */ public String getUserNameFromToken(String token) { String username; try { Claims claims = getClaimsFromToken(token); username = claims.getSubject(); } catch (Exception e) { username = null; } return username; }
/** * 验证token是否还有效 * * @param token 客户端传入的token * @param userDetails 从数据库中查询出来的用户信息 */ public boolean validateToken(String token, UserDetails userDetails) { String username = getUserNameFromToken(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); }
/** * 判断token是否已经失效 */ private boolean isTokenExpired(String token) { Date expiredDate = getExpiredDateFromToken(token); return expiredDate.before(new Date()); }
/** * 从token中获取过期时间 */ private Date getExpiredDateFromToken(String token) { Claims claims = getClaimsFromToken(token); return claims.getExpiration(); }
/** * 根据用户信息生成token */ public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); }
/** * 当原来的token没过期时是可以刷新的 * * @param oldToken 带tokenHead的token */ public String refreshHeadToken(String oldToken) { if (StrUtil.isEmpty(oldToken)) { return null; } String token = oldToken.substring(tokenHead.length()); if (StrUtil.isEmpty(token)) { return null; } //token校验不通过 Claims claims = getClaimsFromToken(token); if (claims == null) { return null; } //如果token已经过期,不支持刷新 if (isTokenExpired(token)) { return null; } //如果token在30分钟之内刚刷新过,返回原token if (tokenRefreshJustBefore(token, 30 * 60)) { return token; } else { claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } }
/** * 判断token在指定时间内是否刚刚刷新过 * * @param token 原token * @param time 指定时间(秒) */ private boolean tokenRefreshJustBefore(String token, int time) { Claims claims = getClaimsFromToken(token); Date created = claims.get(CLAIM_KEY_CREATED, Date.class); Date refreshDate = new Date(); //刷新时间在创建时间的指定时间内 if (refreshDate.after(created) && refreshDate.before(DateUtil.offsetSecond(created, time))) { return true; } return false; }}
复制代码

配置文件 application.yml

这里的 secret 可以用该方法生成


@Test    void generateKey() {        /**         * SECRET 是签名密钥,只生成一次即可,生成方法:         * Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);         * String secretString = Encoders.BASE64.encode(key.getEncoded()); # 本文使用 BASE64 编码         * */        Key key = Keys.secretKeyFor(SignatureAlgorithm.HS512);        String secretString = Encoders.BASE64.encode(key.getEncoded());        System.out.println(secretString);//        Blk1X8JlN4XH4s+Kuc0YUFXv+feyTgVUMycSiKbiL0YhRddy872mCNZBGZIb57Jn2V1RtaFXIxs8TvNPsnG//g==    }
复制代码


jwt: tokenHeader: Authorization secret: Blk1X8JlN4XH4s+Kuc0YUFXv+feyTgVUMycSiKbiL0YhRddy872mCNZBGZIb57Jn2V1RtaFXIxs8TvNPsnG//g== expiration: 604800 tokenHead: 'Bearer '
server: servlet: context-path: /api
复制代码

Controller

AuthController

package top.ryzeyang.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.core.Authentication;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.web.bind.annotation.*;import top.ryzeyang.demo.common.api.CommonResult;import top.ryzeyang.demo.model.dto.UserDTO;import top.ryzeyang.demo.utils.CommonResultUtil;import top.ryzeyang.demo.utils.JwtTokenUtil;
import java.util.Collection;
@RestController@ResponseBody@RequestMapping("/mock")public class AuthController { @Autowired private JwtTokenUtil jwtTokenUtil; @Qualifier("users") @Autowired private UserDetailsService userDetailsService; @Autowired AuthenticationManager authenticationManager;
@GetMapping("/userinfo") public CommonResult<Collection<? extends GrantedAuthority>> getUserInfo(){ Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); return CommonResultUtil.success(authorities); }
/** * 模拟登陆 */ @PostMapping("/login") public CommonResult<String> login(@RequestBody UserDTO userDTO){ String username = userDTO.getUsername(); String password = userDTO.getPassword(); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password); Authentication authenticate = authenticationManager.authenticate(token); SecurityContextHolder.getContext().setAuthentication(authenticate); UserDetails userDetails = userDetailsService.loadUserByUsername(username); String t = jwtTokenUtil.generateToken(userDetails); return CommonResultUtil.success(t); }
}
复制代码

HelloController


package top.ryzeyang.demo.controller;
import org.springframework.security.access.prepost.PreAuthorize;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;

@RestControllerpublic class HelloController {
@GetMapping("/hello") public String hello() { return "hello"; }
@GetMapping("/hello/anonymous") public String hello2() { return "anonymous"; }
@PreAuthorize("hasRole('ADMIN')") @GetMapping("/hello/admin") public String helloAdmin() { return "hello Admin"; }
@PreAuthorize("hasRole('USER')") @GetMapping("/hello/user") public String helloUser() { return "hello user"; }
@PreAuthorize("hasAnyAuthority('system:user:query')") @GetMapping("/hello/user2") public String helloUser2() { return "hello user2"; }
}
复制代码

项目地址在 GitHub 上

地址:SpringSecurity-Vuetify-Permissions-demohttps://github.com/RyzeYang/SpringSecurity-Vuetify-Permissions-demo


现在没法在线预览


结合前端跑起来的样子如下😄



最后

欢迎小伙伴们来一起探讨问题~


如果你觉得本篇文章还不错的话,那拜托再点点赞支持一下呀😝

让我们开始这一场意外的相遇吧!~

欢迎留言!谢谢支持!ヾ(≧▽≦*)o 冲冲冲!!

我是 4ye 咱们下期应该……很快再见!! 😆

如果文章对您有所帮助,欢迎关注公众号 J a v a 4 y e 😆


发布于: 3 小时前阅读数: 4
用户头像

4ye

关注

公众号:J a v a 4 y e 2021.07.19 加入

还未添加个人简介

评论

发布
暂无评论
SpringSecurity+JWT实现前后端分离的使用