SpringBoot整合Security实现权限控制框架(案例详解)

目录

  • 一、前言
  • 二、环境准备
    • 2.1、数据库表
    • 2.2、导入依赖
    • 2.3、配置文件
    • 2.4、WebSecurityConfig Security的主要配置类:
    • 2.5、Security身份验证
    • 2.6、Security授权
    • 2.7、UserDetailsService
    • 2.7、MacLoginUrlAuthenticationEntryPoint
    • 2.8、MyAccessDeniedHandler
    • 2.9、MyLogoutSuccessHandler
    • 2.10、JWT的工具类
  • 三、代码 entity
    • 四、测试
      • 五、总结
        我想每个写项目的人,都肯定会遇到控制权限这个问题.
        例如这个这个链接只能管理员访问,那个链接丫只能超级管理员访问等等,实现方式也有多种多样,控制的粒度也不一样
        以前刚学的时候,不会框架,大都是手写注解+过滤器来进行权限的控制,但这样增加了过滤器的负担。用起来也会稍微有些麻烦,粒度不太好控制。
        用框架的话,就是封装了更多的操作,让一切更简单吧。当然不局限于Security,还有像Shiro安全框架,这两种非常常见。
        一起加油吧!!!
        先看个图舒缓一下,准备开始吧
        SpringBoot整合Security实现权限控制框架(案例详解)
        文章图片

        下面就开始吧!!!

        一、前言 介绍:
        Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
        官网:
        SpringSecurity 最新
        SpringSecurity 5.0.6版本
        优缺点:
        优点
        • Spring Boot 官方提供了大量的非常方便的开箱即用的 Starter ,包括 Spring Security 的 Starter ,使得在 Spring Boot 中使用 Spring Security 变得更加容易。
        • Spring Security功能强大,比较好用。
        缺点
        1. Spring Security 是一个重量级的安全管理框架,
        2. Spring Security概念复杂,配置繁琐(这个确实,没法逃开)
        案例
        我们在访问一个网站时,大都都会设置普通用户能有的权限,然后管理员有的权限,再就是超级管理员等等,这次就是实现这样一个案例。
        项目结构:
        SpringBoot整合Security实现权限控制框架(案例详解)
        文章图片


        二、环境准备
        2.1、数据库表
        CREATE TABLE `account`(`id` int(10) NOT NULL AUTO_INCREMENT,`username` varchar(25) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`role` varchar(25) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; INSERT INTO `account` VALUES (1, 'user', '$2a$10$1MHNdZS.oCICxLRVbnNBZe4CRn9Rk1MVQhasSMhHr0G4BCNQjPpna', 'ROLE_USER'); INSERT INTO `account` VALUES (2, 'admin', '$2a$10$dKkrkgVzaCPX74TvxOjwNuFJjIRJeAuDPKFntwNwRvRHkwIAHV5Q6', 'ROLE_ADMIN'); INSERT INTO `account` VALUES (3, 'super_admin', '$2a$10$CqOXnSp6oks9UTvsops4U.0vMGbUE2Bp28xKaPmlug4W8Mk59Sj8y', 'ROLE_SUPER_ADMIN'); INSERT INTO `account` VALUES (4, 'test', '$2a$10$SQsuH1XfxHdsVmf2nE75wOAE6GHm1nd/xDp/08KYJmtbzJt2J6xIG', 'TEST');


        2.2、导入依赖
        org.springframework.bootspring-boot-starter-parent2.5.2 mysqlmysql-connector-javacom.baomidoumybatis-plus-boot-starter3.4.1io.jsonwebtokenjjwt0.9.0org.springframework.bootspring-boot-starter-securityorg.springframework.bootspring-boot-starter-weborg.projectlomboklomboktruejavax.xml.bindjaxb-api2.3.0

        【SpringBoot整合Security实现权限控制框架(案例详解)】
        2.3、配置文件
        # 应用名称spring.application.name=demo# 应用服务 WEB 访问端口server.port=8080spring.datasource.name=defaultDataSource# 数据库连接地址spring.datasource.url=jdbc:mysql://localhost:3306/security?serverTimezone=UTC# 数据库用户名&密码:spring.datasource.username=rootspring.datasource.password=123456mybatis-plus.mapper-locations=classpath:mapper/**/*.xmllogging.level.com.crush.security.mapper=DEBUG# token 存活时间token.expire=3600000token.key=123456


        2.4、WebSecurityConfig Security的主要配置类:
        import com.crush.security.auth.filter.JwtAuthenticationFilter; import com.crush.security.auth.filter.JwtAuthorizationFilter; import com.crush.security.auth.handle.MacLoginUrlAuthenticationEntryPoint; import com.crush.security.auth.handle.MyAccessDeniedHandler; import com.crush.security.auth.handle.MyLogoutSuccessHandler; import com.crush.security.auth.service.UserDetailServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; 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.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * @author crush */@Configuration@EnableWebSecurity//启用全局配置@EnableGlobalMethodSecurity(prePostEnabled = true)public class WebSecurityConfig extends WebSecurityConfigurerAdapter {/**放行的路径*/private final String[] PATH_RELEASE = {"/login","/all"}; /***根据用户名找到用户*/@Autowiredprivate UserDetailServiceImpl userDetailService; @Autowiredprivate MacLoginUrlAuthenticationEntryPoint macLoginUrlAuthenticationEntryPoint; @Autowiredprivate MyAccessDeniedHandler myAccessDeniedHandler; @Autowiredprivate MyLogoutSuccessHandler myLogoutSuccessHandler; @Overrideprotected void configure(HttpSecurity http) throws Exception {http.cors().and().csrf().disable(); http.authorizeRequests()/**antMatchers (这里的路径)permitAll 这里是允许所有人 访问*/.antMatchers(PATH_RELEASE).permitAll()/** 映射任何请求 */.anyRequest()/** 指定任何经过身份验证的用户都允许使用URL。*/.authenticated()/** 指定支持基于表单的身份验证 */.and().formLogin().permitAll()/** 允许配置异常处理。可以自己传值进去 使用WebSecurityConfigurerAdapter时,将自动应用此WebSecurityConfigurerAdapter 。*/.and().exceptionHandling()/** 设置要使用的AuthenticationEntryPoint。macLoginUrlAuthenticationEntryPoint验证是否登录*/.authenticationEntryPoint(macLoginUrlAuthenticationEntryPoint)/** 指定要使用的AccessDeniedHandler处理拒绝访问失败。*/.accessDeniedHandler(myAccessDeniedHandler)/** 提供注销支持。 使用WebSecurityConfigurerAdapter时,将自动应用此WebSecurityConfigurerAdapter 。*默认设置是访问URL “ / logout”将使HTTP会话无效,清理配置的所有rememberMe()身份验证,清除SecurityContextHolder ,*然后重定向到“ / login?success”,从而注销用户*/.and().logout().logoutSuccessHandler(myLogoutSuccessHandler)/** 处理身份验证表单提交。 授予权限 */.and().addFilter(new JwtAuthenticationFilter(authenticationManager()))/** 处理HTTP请求的BASIC授权标头,然后将结果放入SecurityContextHolder 。 */.addFilter(new JwtAuthorizationFilter(authenticationManager()))/**不需要session */.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); }@Overridepublic void configure(WebSecurity web) throws Exception {super.configure(web); }/*** * 因为使用了BCryptPasswordEncoder来进行密码的加密,所以身份验证的时候也的用他来判断哈、,* @param auth* @throws Exception*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder()); }/** * 密码加密*/@BeanPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder(); }}


        2.5、Security身份验证
        import com.crush.security.entity.MyUser; import com.crush.security.utils.JwtTokenUtils; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; /** * 处理身份验证表单提交。 * * @author crush */public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {private AuthenticationManager authenticationManager; public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {this.authenticationManager = authenticationManager; }/*** 执行实际的身份验证。* 该实现应执行以下操作之一:* 返回已验证用户的已填充验证令牌,指示验证成功* 返回null,表示身份验证过程仍在进行中。 在返回之前,实现应执行完成该过程所需的任何其他工作。* 如果身份验证过程失败,则抛出AuthenticationException*/@Overridepublic Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {//输入流中获取到登录的信息try {MyUser loginUser = new ObjectMapper().readValue(request.getInputStream(), MyUser.class); logger.info("loginUser===>" + loginUser); /*** authenticate* 尝试对传递的Authentication对象进行身份Authentication ,* 如果成功,则返回完全填充的Authentication对象(包括授予的权限)* */return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList<>())); } catch (IOException e) {e.printStackTrace(); return null; }}/*** 成功验证后调用的方法* 如果验证成功,就生成token并返回*/@Overrideprotected void successfulAuthentication(HttpServletRequest request,HttpServletResponse response,FilterChain chain,Authentication authResult) throws IOException, ServletException {// 查看源代码会发现调用getPrincipal()方法会返回一个实现了`UserDetails`接口的对象// 所以就是JwtUser啦MyUser user = (MyUser) authResult.getPrincipal(); String role = ""; // 因为在JwtUser中存了权限信息,可以直接获取,由于只有一个角色就这么干了Collection authorities = user.getAuthorities(); for (GrantedAuthority authority : authorities) {role = authority.getAuthority(); }// 根据用户名,角色创建token并返回json信息String token = JwtTokenUtils.createToken(user.getUsername(), role, false); user.setPassword(null); user.setToken(JwtTokenUtils.TOKEN_PREFIX + token); response.setStatus(HttpServletResponse.SC_OK); response.setHeader("token", JwtTokenUtils.TOKEN_PREFIX + token); response.setContentType("application/json; charset=utf-8"); PrintWriter writer = response.getWriter(); writer.write(new ObjectMapper().writeValueAsString(user)); }/*** 验证失败时候调用的方法*/@Overrideprotected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType("application/json; charset=utf-8"); PrintWriter writer = response.getWriter(); writer.write(new ObjectMapper().writeValueAsString( "登录失败,账号或密码错误")); }}


        2.6、Security授权
        import com.crush.security.utils.JwtTokenUtils; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; 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.Collections; /** * 处理HTTP请求的BASIC授权标头,然后将结果放入SecurityContextHolder 。 */public class JwtAuthorizationFilter extends BasicAuthenticationFilter {public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {super(authenticationManager); }@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain) throws IOException, ServletException {String tokenHeader = request.getHeader(JwtTokenUtils.TOKEN_HEADER); // 如果请求头中没有Authorization信息则直接放行了if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtils.TOKEN_PREFIX)) {chain.doFilter(request, response); return; }// 如果请求头中有token,则进行解析,并且设置认证信息SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader)); super.doFilterInternal(request, response, chain); }/** * 这里从token中获取用户信息并新建一个token*/private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, ""); String username = JwtTokenUtils.getUsername(token.trim()); String role = JwtTokenUtils.getUserRole(token); if (username != null) {return new UsernamePasswordAuthenticationToken(username, null,Collections.singleton(new SimpleGrantedAuthority(role))); }return null; }}


        2.7、UserDetailsService
        UserDetailServiceImpl 实现了UserDetailsService,用来加载用户特定数据的核心接口。
        import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.crush.security.entity.MyUser; import com.crush.security.service.IMyUserService; import lombok.extern.slf4j.Slf4j; 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; @Slf4j@Servicepublic class UserDetailServiceImpl implements UserDetailsService {finalIMyUserService userService; public UserDetailServiceImpl(IMyUserService userService) {this.userService = userService; }@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {MyUser user = userService.getOne(new QueryWrapper().eq("username", username)); return user; }}


        2.7、MacLoginUrlAuthenticationEntryPoint
        /** * * 身份验证没有通过回调 */@Componentpublic class MacLoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); httpServletResponse.setContentType("application/json; charset=utf-8"); PrintWriter writer = httpServletResponse.getWriter(); writer.write(new ObjectMapper().writeValueAsString("未登录!")); }}


        2.8、MyAccessDeniedHandler
        /** * 权限不足回调 */@Componentpublic class MyAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {httpServletResponse.setContentType("application/json; charset=utf-8"); httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); PrintWriter writer = httpServletResponse.getWriter(); writer.write(new ObjectMapper().writeValueAsString("不好意思,你的权限不足!")); }}


        2.9、MyLogoutSuccessHandler
        /** * 退出回调 */@Componentpublic class MyLogoutSuccessHandler implements LogoutSuccessHandler {@Overridepublic void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {httpServletResponse.setStatus(HttpServletResponse.SC_OK); httpServletResponse.setContentType("application/json; charset=utf-8"); PrintWriter writer = httpServletResponse.getWriter(); writer.write(new ObjectMapper().writeValueAsString( "退出成功")); }}


        2.10、JWT的工具类
        生成token
        package com.crush.security.utils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import java.util.Date; import java.util.HashMap; public class JwtTokenUtils {public static final String TOKEN_HEADER = "Authorization"; public static final String TOKEN_PREFIX = "Bearer "; private static final String SECRET = "jwtsecretdemo"; private static final String ISS = "echisan"; /*** 过期时间是3600秒,既是1个小时*/private static final long EXPIRATION = 3600L; /*** 选择了记住我之后的过期时间为7天*/private static final long EXPIRATION_REMEMBER = 604800L; // 添加角色的keyprivate static final String ROLE_CLAIMS = "rol"; /*** 修改一下创建token的方法** @param username* @param role* @param isRememberMe* @return*/public static String createToken(String username, String role, boolean isRememberMe) {String token = null; try {long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION; HashMap map = new HashMap<>(); map.put(ROLE_CLAIMS, role); token = Jwts.builder().signWith(SignatureAlgorithm.HS512, SECRET)// 这里要早set一点,放到后面会覆盖别的字段.setClaims(map).setIssuer(ISS).setSubject(username).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)).compact(); } catch (ExpiredJwtException e) {e.getClaims(); }return token; }/*** 从token中获取用户名** @param token* @return*/public static String getUsername(String token) {return getTokenBody(token).getSubject(); }/*** 从token中获取roles** @param token* @return*/public static String getUserRole(String token) {return (String) getTokenBody(token).get(ROLE_CLAIMS); }/*** 是否已过期** @param token* @return*/public static boolean isExpiration(String token) {return getTokenBody(token).getExpiration().before(new Date()); }private static Claims getTokenBody(String token) {return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); }public static void main(String[] args) {BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String user = encoder.encode("test"); System.out.println(user); }}

        弄完上面这些,相关配置就都搞定了,剩下就是最简单的编码啦。

        三、代码 entity
        @Data@EqualsAndHashCode(callSuper = false)@TableName("account")public class MyUser implements Serializable, UserDetails {private static final long serialVersionUID = 1L; private int id; private String username; private String password; // 1:启用 , 0:禁用@TableField(exist = false)private Integer enabled = 1; // 1:锁住 , 0:未锁@TableField(exist = false)private Integer locked = 0; private String role; @TableField(exist = false)private String token; //授权@Overridepublic Collection getAuthorities() {List authorities = new ArrayList<>(); SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role); authorities.add(authority); return authorities; }@Overridepublic boolean isAccountNonExpired() { return true; }@Overridepublic boolean isAccountNonLocked() { return locked == 0; }@Overridepublic boolean isCredentialsNonExpired() {return true; }@Overridepublic boolean isEnabled() { return enabled == 1; }}

        mapper
        import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.crush.security.entity.MyUser; import org.springframework.stereotype.Repository; @Repositorypublic interface MyUserMapper extends BaseMapper {}

        service、impl
        import com.baomidou.mybatisplus.extension.service.IService; import com.crush.security.entity.MyUser; public interface IMyUserService extends IService {}

        import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.crush.security.entity.MyUser; import com.crush.security.mapper.MyUserMapper; import com.crush.security.service.IMyUserService; import org.springframework.stereotype.Service; @Servicepublic class MyUserServiceImpl extends ServiceImpl implements IMyUserService {}

        controller
        package com.crush.security.controller; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestControllerpublic class UserController {@RequestMapping("/all")String all() {return "在WebSecurityConfig中配置了放行,任何人都可以进行访问"; }@PreAuthorize("permitAll()")@RequestMapping("/test")String test() {return "所有登录的人都可以访问"; }@PreAuthorize("hasRole('USER')")@RequestMapping("/user/userList")String userList() {return "role: user"; }@PreAuthorize("hasRole('ADMIN')")@RequestMapping("/admin/updateUser")String updateUser() {return "role: admin"; }@PreAuthorize("hasRole('SUPER_ADMIN')")@RequestMapping("/admin/superAdmin")String superAdmin() {return "role: superAdmin"; }@PreAuthorize("hasAnyRole('ADMIN','USER')")@RequestMapping("/userAndAdmin")String userAndAdminTest() {return "role: admin and user"; }@PreAuthorize("hasAnyRole('ADMIN')or hasAnyRole('SUPER_ADMIN')")@RequestMapping("/AdminAndSuperAdminTest")String AdminAndSuperAdminTest() {return "role: admin and super_admin"; }// hasAnyAuthority() 也是可以多个字符串 权限验证,可以不跟ROLE_前缀@PreAuthorize("hasAuthority('TEST') ")@RequestMapping("/ceshi2")String ceshi2() {return "hasAuthority:权限验证,不过查的也是role那个字段,不过不用拼接上ROLE而已"; }}


        四、测试 :我使用的测试工具是Postman,另外login接口接收的数据是需要JSON类型的。
        1)登录
        注意这里的token,我们是需要把他记住,下次去请求要携带上。
        SpringBoot整合Security实现权限控制框架(案例详解)
        文章图片

        2)测试管理员
        SpringBoot整合Security实现权限控制框架(案例详解)
        文章图片

        3)测试hasAnyAuthority ()注解
        hasAnyAuthority() 也是可以多个字符串 权限验证,可以不跟ROLE_前缀
        SpringBoot整合Security实现权限控制框架(案例详解)
        文章图片

        SpringBoot整合Security实现权限控制框架(案例详解)
        文章图片


        五、总结 Security框架和SpringBoot集成,其实上手特别快,但是如果要想研究的比较深刻的话,我觉得是比较困难的,上文讲过,security是属于一个重量级的框架,里面很多东西特别多。使用方面肯定是没有任何问题的。
        到此这篇关于SpringBoot整合Security安全框架、控制权限的文章就介绍到这了,更多相关SpringBoot整合Security权限控制框架内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

          推荐阅读