SpringBoot 集成 SpringSecurity + MySQL + JWT 无太多理论,直接盘
一般用于Web管理系统
可以先看 SpringBoot SpringSecurity 基于内存的使用介绍
本文介绍如何整合 SpringSecurity + MySQL + JWT
数据结构
数据库脚本:https://gitee.com/VipSoft/VipBoot/blob/develop/vipsoft-security/sql/Security.sql
常规权限管理数据结构设计,三张常规表:用户、角色、菜单,通过用户和角色的关系,角色和菜单(权限)的关系,实现用户和菜单(按钮)的访问控制权
用户登录
- SecurityConfig 中添加登录接口匿名访问配置
.antMatchers("/auth/login", "/captchaImage").anonymous()
BCryptPasswordEncoder
密码加密方式
- POST 登录接口
/auth/login
调用
AuthorizationController.login
用户登录接口做入参、图形验证码等验证。
- 实现
UserDetailsService
接口
根据用户名,去数据库获取用户信息、权限获取等
- 密码验证
AuthorizationService.login
调用
authenticationManager.authenticate(authenticationToken)
看密码是否正确可以在此集合 Redis 做失败次数逻辑处理
- 通过JWT 生成 Token
调用
jwtUtil.generateToken(userId)
生成Token令牌将 用户信息放入 Redis
剔除其它已登录的用户(如果需要)
- 返回Map对象给前端
接口权限认证
- 获取
request.getHeader
中的token信息
AuthenticationTokenFilter.doFilterInternal
解析 Token 中的用户ID 去 Redis 缓存中获取用户信息
将信息赋到
SecurityContextHolder.getContext().setAuthentication(authenticationToken)
中,供权限验证获取用户信息使用, SecurityContextHolder使用了ThreadLocal机制来保存每个使用者的安全上下文
- 接口权限配置
UserController
类的方法上,加了@PreAuthorize("@ps.hasAnyPermi('system:user:list')")
用来做权限控制
- 访问权限控制
PermissionService.hasAnyPermi
判断,用户所拥有的权限,是否包含@PreAuthorize("@ps.hasAnyPermi('system:user:list')")
中配置的权限,包含则有权访问
用户登录代码
SecurityConfig
package com.vipsoft.web.config; import com.vipsoft.web.security.AuthenticationEntryPointImpl; import com.vipsoft.web.security.AuthenticationTokenFilter; import com.vipsoft.web.security.LogoutSuccessHandlerImpl; 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.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.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 自定义用户认证逻辑 */ @Autowired private UserDetailsService userDetailsService; /** * 认证失败处理类 */ @Autowired private AuthenticationEntryPointImpl unauthorizedHandler; /** * 退出处理类 */ @Autowired private LogoutSuccessHandlerImpl logoutSuccessHandler; /** * token认证过滤器 */ @Autowired private AuthenticationTokenFilter authenticationTokenFilter; /** * 解决 无法直接注入 AuthenticationManager * * @return * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 强散列哈希加密实现 * 必须 Bean 的形式实例化,否则会报 :Encoded password does not look like BCrypt */ @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } /** * 配置用户身份的configure()方法 * * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); } /** * 配置用户权限的configure()方法 * * @param httpSecurity * @throws Exception */ @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 验证码captchaImage 允许匿名访问 .antMatchers("/auth/login", "/captchaImage").anonymous() .antMatchers( HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/webSocket/**" ).permitAll() // swagger 文档 .antMatchers("/swagger-ui.html").permitAll() .antMatchers("/swagger-resources/**").permitAll() .antMatchers("/webjars/**").permitAll() .antMatchers("/*/api-docs").permitAll() .antMatchers("/druid/**").permitAll() // 放行OPTIONS请求 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 所有请求都需要认证 .anyRequest().authenticated() // .and().apply(this.securityConfigurerAdapter()); .and() //设置跨域, 如果不设置, 即使配置了filter, 也不会生效 .cors() .and() .headers().frameOptions().disable(); httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); // 添加JWT filter httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } }
AuthenticationController.login
public Map<String, Object> login(SysUser user) { String username = user.getUserName(); String password = user.getPassword(); Authentication authentication; try { //该方法会去调用UserDetailsServiceImpl.loadUserByUsername UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); authentication = authenticationManager.authenticate(authenticationToken); } catch (AuthenticationException ex) { Long incr = 3L; // Redis 实现 if (incr > 5) { logger.error("{} 账户连续{}次登录失败,账户被锁定30分钟", username, incr); throw new LockedException("密码连续输入错误次数过多,账户已被锁定!"); } throw new BadCredentialsException("您输入的用户名、密码或验证码不正确,为保证账户安全,连续5次输入错误,系统将锁定您的账户30分钟,当前剩余:" + (PASSOWRD_MAX_ERROR_COUNT - incr) + "次", ex); } SecurityContextHolder.getContext().setAuthentication(authentication); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); String userId = loginUser.getUser().getUserId().toString(); // 生成令牌 String token = jwtUtil.generateToken(userId); Map<String, Object> resultMap = new HashMap(); resultMap.put("AccessToken", token); resultMap.put("UserId", userId); // Redis 保存上线信息 // UserAgent userAgent // 踢掉已登录用户 return resultMap; }
UserDetailsServiceImpl
@Service public class UserDetailsServiceImpl implements UserDetailsService { Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private ISysUserService userService; @Autowired private ISysMenuService menuService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser user = userService.selectUserByUserName(username); if (user == null) { logger.info("登录用户:{} 不存在.", username); throw new UsernameNotFoundException("登录用户:" + username + " 不存在"); } else if ("1".equals(user.getDelFlag())) { logger.info("登录用户:{} 已被删除.", username); throw new CustomException("对不起,您的账号:" + username + " 已被删除"); } else if ("1".equals(user.getStatus())) { logger.info("登录用户:{} 已被停用.", username); throw new CustomException("对不起,您的账号:" + username + " 已停用"); } Set<String> perms = new HashSet<>(); // 管理员拥有所有权限 if (user.isAdmin()) { perms.add("*:*:*"); } else { perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId())); } return new LoginUser(user, perms); } }
接口权限认证代码
AuthenticationTokenFilter
@Component public class AuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { LoginUser loginUser = jwtUtil.getLoginUser(request); if (loginUser != null && SecurityUtils.getAuthentication() == null) { jwtUtil.verifyToken(loginUser); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); //SecurityContextHolder使用了ThreadLocal机制来保存每个使用者的安全上下文,确保PermissionService判断权限时可以获得当前LoginUser信息 SecurityUtils.setAuthentication(authenticationToken); } chain.doFilter(request, response); } }
定义权限验证类 PermissionService
package com.vipsoft.web.security; import cn.hutool.core.util.StrUtil; import com.vipsoft.web.utils.SecurityUtils; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import java.util.Arrays; import java.util.Set; /** * 自定义权限实现 */ @Service("ps") public class PermissionService { /** * 所有权限标识 */ private static final String ALL_PERMISSION = "*:*:*"; /** * 管理员角色权限标识 */ private static final String SUPER_ADMIN = "admin"; private static final String ROLE_DELIMETER = ","; private static final String PERMISSION_DELIMETER = ","; /** * 对用户请求的接口进行验证,看接口所需要的权限,当前用户是否包括 * * @param permissions 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表,如:system:user:add,system:user:edit * @return 用户是否具有以下任意一个权限 */ public boolean hasAnyPermi(String permissions) { if (StrUtil.isEmpty(permissions)) { return false; } LoginUser loginUser = SecurityUtils.getCurrentUser(); //去SecurityContextHolder.getContext()中获取登录用户信息 if (loginUser == null || CollectionUtils.isEmpty(loginUser.getPermissions())) { return false; } Set<String> authorities = loginUser.getPermissions(); String[] perms = permissions.split(PERMISSION_DELIMETER); boolean hasPerms = Arrays.stream(perms).anyMatch(authorities::contains); //是Admin权限 或者 拥有接口所需权限时 return permissions.contains(ALL_PERMISSION) || hasPerms; } }
详细代码见:https://gitee.com/VipSoft/VipBoot/tree/develop/vipsoft-security
源代码摘自:若依后台管理系统