SpringBoot 整合Spring Security + JWT 实现前后端分离项目的认证授权
以下是伪代码,要根据自己的业务自行修改
引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
还用了jwt、redis、fastjson 等 如果添加了就不用添加
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <!-- 阿里JSON解析器 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.80</version> </dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
JAVA结合 JSON Web Token(JWT) 工具类参考:https://www.cnblogs.com/pxblog/p/12954756.html
SpringBoot 整合Redis 参考:https://www.cnblogs.com/pxblog/p/12980634.html
Spring Boot全局异常处理参考:https://www.cnblogs.com/pxblog/p/14307697.html
SercurityConfig.java
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; 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.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.web.filter.CorsFilter; import javax.annotation.Resource; /** * @author yvioo。 */ @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SercurityConfig extends WebSecurityConfigurerAdapter { /** * token认证过滤器 */ @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; /** * 认证失败处理类 */ @Autowired private AuthenticationEntryPointImpl unauthorizedHandler; /** * 退出处理类 */ @Autowired private LogoutSuccessHandlerImpl logoutSuccessHandler; /** * 跨域过滤器 */ @Resource private CorsFilter corsFilter; /** * 密码加密方式 * 创建BCryptPasswordEncoder注入容器 * * @return */ @Bean public PasswordEncoder passwordEncoder() { //如果不加密,使用 NoOpPasswordEncoder.getInstance(); return new BCryptPasswordEncoder(); } /** * 解决 无法直接注入 AuthenticationManager * * @return * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * anyRequest | 匹配所有请求路径 * access | SpringEl表达式结果为true时可以访问 * anonymous | 匿名可以访问 * denyAll | 用户不能访问 * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 * hasRole | 如果有参数,参数表示角色,则其角色可以访问 * permitAll | 用户可以任意访问 * rememberMe | 允许通过remember-me登录的用户访问 * authenticated | 用户登录后可访问 */ @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity // CSRF禁用,因为不使用session .csrf().disable() // 认证失败处理类 如果没有可以不加 .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() // 基于token,所以不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // 过滤请求 .authorizeRequests() // 对于登录login 注册register 验证码captchaImage 允许匿名访问 .antMatchers("/login", "/register", "/captchaImage").anonymous() .antMatchers( HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**" ).permitAll() .antMatchers("/swagger-ui.html").anonymous() .antMatchers("/swagger-resources/**").anonymous() .antMatchers("/webjars/**").anonymous() .antMatchers("/*/api-docs").anonymous() .antMatchers("/druid/**").anonymous() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated() .and() .headers().frameOptions().disable(); httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); // 添加JWT filter httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 添加CORS filter httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class); httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class); } }
User.java
import lombok.Data; /** * 用户对象 * @author 。 */ @Data public class User { private Long userId; private String userName; private String password; private boolean admin; }
LoginUser.java
import com.alibaba.fastjson.annotation.JSONField; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.Set; /** * 登录用户身份权限 * * @author */ @AllArgsConstructor @NoArgsConstructor @Data public class LoginUser implements UserDetails { private static final long serialVersionUID = 1L; /** * 用户ID */ private Long userId; /** * 用户唯一标识 */ private String token; /** * 登录时间 */ private Long loginTime; /** * 过期时间 */ private Long expireTime; /** * 权限列表 */ private Set<String> permissions; /** * 用户信息 */ private User user; public LoginUser(User user, Set<String> permissions) { this.user = user; this.permissions = permissions; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @JSONField(serialize = false) @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUserName(); } /** * 账户是否未过期,过期无法验证 */ @JSONField(serialize = false) @Override public boolean isAccountNonExpired() { return true; } /** * 指定用户是否解锁,锁定的用户无法进行身份验证 * * @return */ @JSONField(serialize = false) @Override public boolean isAccountNonLocked() { return true; } /** * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证 * * @return */ @JSONField(serialize = false) @Override public boolean isCredentialsNonExpired() { return true; } /** * 是否可用 ,禁用的用户不能身份验证 * * @return */ @JSONField(serialize = false) @Override public boolean isEnabled() { return true; } }
LoginController.java
import com.example.security.entity.R; import com.example.security.service.UserService; import com.example.security.entity.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; /** * 登录控制器 * @author 。 */ @RestController public class LoginController { @Autowired private UserService userService; /** * R类参考:https://www.cnblogs.com/pxblog/p/13792038.html * @param user * @return */ @PostMapping(value = "/login") public R login(@RequestBody User user){ //返回生成的token return R.success(userService.login(user)); } @GetMapping(value = "/hello") @PreAuthorize("hasAuthority('show:hello')") public String hello(){ return "hello"; } }
UserService.java
import com.example.security.entity.LoginUser; import com.example.security.entity.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; /** * @author 。 */ @Service public class UserService { @Autowired private TokenService tokenService; @Autowired private AuthenticationManager authenticationManager; public String login(User user) { // 用户验证 Authentication authentication = null; try { // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername authentication = authenticationManager .authenticate(new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword())); } catch (Exception e) { if (e instanceof BadCredentialsException) { //这里要自行添加全局异常处理,参考:https://www.cnblogs.com/pxblog/p/14307697.html throw new RuntimeException("用户名和密码不匹配"); } else { throw new RuntimeException(e.getMessage()); } } //没有异常 表示登录成功 LoginUser loginUser = (LoginUser) authentication.getPrincipal(); return tokenService.createToken(loginUser); } public User selectUserByUserName(String username) { /** * 查询用户 */ return null; } }
UserDetailsServiceImpl.java
import com.example.security.entity.LoginUser; import com.example.security.entity.User; import lombok.extern.slf4j.Slf4j; 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.Service; import java.util.Arrays; import java.util.HashSet; import java.util.Set; /** * @author 。 */ @Service @Slf4j public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserService userService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { /** * 这里根据用户名去数据库查询用户是否存在,这里按照自己的逻辑来写即可 * 可能还有禁用,或者已经被逻辑删除 的判断 */ User user = userService.selectUserByUserName(username); if (user==null) { log.info("登录用户:{} 不存在.", username); throw new RuntimeException("登录用户:" + username + " 不存在"); } /** * 判断成功后,封装成一个对象返回 */ return new LoginUser(user, getMenuPermission(user)); } public Set<String> getMenuPermission(User user) { Set<String> perms = new HashSet<String>(); if (user.isAdmin()) { /** * 管理员拥有所有权限 */ perms.add("*:*:*"); } else { /** * 根据用户ID去数据库查询菜单权限,封装成一个list返回,这里只是举例 * 注意:需要把权限换成以下这种方式 这里权限和 @PreAuthorize 上的对应 */ perms.addAll(Arrays.asList("show:hello")); } return perms; } }
TokenService.java
import com.example.security.config.JWTUtils; import com.example.security.config.RedisService; import com.example.security.entity.LoginUser; import io.jsonwebtoken.Claims; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.util.concurrent.TimeUnit; /** * token验证处理 * * @author 。 */ @Component public class TokenService { /** * 令牌自定义标识 * 一般是:token */ @Value("${token.header}") private String header; /** * 令牌有效期 * (默认30分钟) */ @Value("${token.expireTime}") private int expireTime; /** * 令牌前缀 */ public static final String LOGIN_USER_KEY = "login_user_key"; /** * 登录用户 redis key */ public static final String LOGIN_TOKEN_KEY = "login_tokens:"; protected static final long MILLIS_SECOND = 1000; protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND; private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L; /** * 整合redis参考:https://www.cnblogs.com/pxblog/p/12980634.html */ @Autowired private RedisService redisService; /** * 获取用户身份信息 * * @return 用户信息 */ public LoginUser getLoginUser(HttpServletRequest request) { // 获取请求携带的令牌 String token = getToken(request); if (StringUtils.isNotEmpty(token)) { try { Claims claims = JWTUtils.parseJWT(token); // 解析对应的权限以及用户信息 String uuid = (String) claims.get(LOGIN_USER_KEY); String userKey = getTokenKey(uuid); LoginUser user = redisService.getCacheObject(userKey); return user; } catch (Exception e) { } } return null; } /** * 设置用户身份信息 * 修改用户资料,或者修改用户角色的时候修改同步缓存数据 */ public void setLoginUser(LoginUser loginUser) { if (loginUser != null && StringUtils.isNotEmpty(loginUser.getToken())) { refreshToken(loginUser); } } /** * 删除用户身份信息 */ public void delLoginUser(String token) { if (StringUtils.isNotEmpty(token)) { String userKey = getTokenKey(token); redisService.del(userKey); } } /** * 创建令牌 * JWTUtils 参考:https://www.cnblogs.com/pxblog/p/12954756.html * @param loginUser 用户信息 * @return 令牌 */ public String createToken(LoginUser loginUser) { String token = JWTUtils.createJWT(loginUser.getUserId().toString(),"yvioo",loginUser.getUsername(),null,loginUser.getExpireTime()); loginUser.setToken(token); refreshToken(loginUser); return token; } /** * 验证令牌有效期,相差不足20分钟,自动刷新缓存 * * @param loginUser * @return 令牌 */ public void verifyToken(LoginUser loginUser) { long expireTime = loginUser.getExpireTime(); long currentTime = System.currentTimeMillis(); if (expireTime - currentTime <= MILLIS_MINUTE_TEN) { refreshToken(loginUser); } } /** * 刷新令牌有效期 * * @param loginUser 登录信息 */ public void refreshToken(LoginUser loginUser) { loginUser.setLoginTime(System.currentTimeMillis()); loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE); // 根据uuid将loginUser缓存 String userKey = getTokenKey(loginUser.getToken()); redisService.set(userKey, loginUser, expireTime, TimeUnit.MINUTES); } /** * 获取请求token * * @param request * @return token */ private String getToken(HttpServletRequest request) { String token = request.getHeader(header); return token; } private String getTokenKey(String uuid) { return LOGIN_TOKEN_KEY + uuid; } }
JwtAuthenticationTokenFilter.java
import com.example.security.entity.LoginUser; import com.example.security.service.TokenService; import org.apache.commons.lang3.ObjectUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * token过滤器 验证token有效性 * * @author 。 */ @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private TokenService tokenService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { LoginUser loginUser = tokenService.getLoginUser(request); if (!ObjectUtils.isEmpty(loginUser)){ //token不为空 验证token tokenService.verifyToken(loginUser); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } chain.doFilter(request, response); } }
LogoutSuccessHandlerImpl.java
import com.alibaba.fastjson.JSON; import com.example.security.entity.LoginUser; import com.example.security.entity.R; import com.example.security.service.TokenService; import org.apache.commons.lang3.ObjectUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 自定义退出处理类 返回成功 * * @author 。 */ @Configuration public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler { @Autowired private TokenService tokenService; /** * 退出处理 * * @return */ @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { LoginUser loginUser = tokenService.getLoginUser(request); if (!ObjectUtils.isEmpty(loginUser)) { // 删除用户缓存记录 tokenService.delLoginUser(loginUser.getToken()); } /** * 这里可以自己封装返回前端对象,根据自己的来 */ renderString(response, JSON.toJSONString(R.success("退出成功"))); } /** * 将字符串渲染到客户端 * * @param response 渲染对象 * @param string 待渲染的字符串 */ public static void renderString(HttpServletResponse response, String string) { try { response.setStatus(200); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); response.getWriter().print(string); } catch (IOException e) { e.printStackTrace(); } } }
AuthenticationEntryPointImpl.java
import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.Serializable; /** * 认证失败处理类 * * @author . */ @Component @Slf4j public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { log.error("认证失败了"); } }
以及跨域处理
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * * @author . */ @Configuration public class ResourcesConfig implements WebMvcConfigurer { /** * 跨域配置 */ @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); // 设置访问源地址 config.addAllowedOriginPattern("*"); // 设置访问源请求头 config.addAllowedHeader("*"); // 设置访问源请求方法 config.addAllowedMethod("*"); // 有效期 1800秒 config.setMaxAge(1800L); // 添加映射路径,拦截一切请求 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); // 返回新的CorsFilter return new CorsFilter(source); } }
-----------------------有任何问题可以在评论区评论,也可以私信我,我看到的话会进行回复,欢迎大家指教------------------------
(蓝奏云官网有些地址失效了,需要把请求地址lanzous改成lanzoux才可以)