首页 >> 资讯 > >> 内容页

SpringBoot 集成 SpringSecurity + MySQL + JWT 附源码,废话不多直接盘_环球短讯

2023-05-04 13:28:05 来源:博客园
分享到:

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 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 resultMap = new HashMap();    resultMap.put("AccessToken", token);    resultMap.put("UserId", userId);    // Redis 保存上线信息    // UserAgent userAgent    // 踢掉已登录用户    return resultMap;}

UserDetailsServiceImpl

@Servicepublic 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 perms = new HashSet<>();        // 管理员拥有所有权限        if (user.isAdmin()) {            perms.add("*:*:*");        } else {            perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));        }        return new LoginUser(user, perms);    }}
接口权限认证代码![image](https://img2023.cnblogs.com/blog/80824/202304/80824-20230425143653875-1265259330.png)![image](https://img2023.cnblogs.com/blog/80824/202304/80824-20230425143806480-1424421615.png)AuthenticationTokenFilter```java@Componentpublic class AuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil;
@Overrideprotected 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````javapackage 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 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源代码摘自:若依后台管理系统

关键词:

Copyright ©  2015-2022 南非母婴网版权所有  备案号:沪ICP备2022005074号-13   联系邮箱:58 55 97 3@qq.com