当前位置: 代码迷 >> 综合 >> Spring Boot 2.X(十八):集成 Spring Security-登录认证和权限控制
  详细解决方案

Spring Boot 2.X(十八):集成 Spring Security-登录认证和权限控制

热度:77   发布时间:2023-09-05 17:42:20.0

前言

在企业项目开发中,对系统的安全和权限控制往往是必需的,常见的安全框架有 Spring Security、Apache Shiro 等。本文主要简单介绍一下 Spring Security,再通过 Spring Boot 集成开一个简单的示例。

Spring Security

什么是 Spring Security?

Spring Security 是一种基于 Spring AOP 和 Servlet 过滤器 Filter 的安全框架,它提供了全面的安全解决方案,提供在 Web 请求和方法调用级别的用户鉴权和权限控制。

Web 应用的安全性通常包括两方面:用户认证(Authentication)和用户授权(Authorization)。

用户认证指的是验证某个用户是否为系统合法用户,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码,系统通过校验用户名和密码来完成认证。

用户授权指的是验证某个用户是否有权限执行某个操作。

2.原理

Spring Security 功能的实现主要是靠一系列的过滤器链相互配合来完成的。以下是项目启动时打印的默认安全过滤器链(集成5.2.0):

[org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@5054e546,org.springframework.security.web.context.SecurityContextPersistenceFilter@7b0c69a6,org.springframework.security.web.header.HeaderWriterFilter@4fefa770,org.springframework.security.web.csrf.CsrfFilter@6346aba8,org.springframework.security.web.authentication.logout.LogoutFilter@677ac054,org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@51430781,org.springframework.security.web.savedrequest.RequestCacheAwareFilter@4203d678,org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@625e20e6,org.springframework.security.web.authentication.AnonymousAuthenticationFilter@19628fc2,org.springframework.security.web.session.SessionManagementFilter@471f8a70,org.springframework.security.web.access.ExceptionTranslationFilter@3e1eb569,org.springframework.security.web.access.intercept.FilterSecurityInterceptor@3089ab62
]
  • WebAsyncManagerIntegrationFilter
  • SecurityContextPersistenceFilter
  • HeaderWriterFilter
  • CsrfFilter
  • LogoutFilter
  • UsernamePasswordAuthenticationFilter
  • RequestCacheAwareFilter
  • SecurityContextHolderAwareRequestFilter
  • AnonymousAuthenticationFilter
  • SessionManagementFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor

详细解读可以参考:https://blog.csdn.net/dushiwodecuo/article/details/78913113

3.核心组件

SecurityContextHolder

用于存储应用程序安全上下文(Spring Context)的详细信息,如当前操作的用户对象信息、认证状态、角色权限信息等。默认情况下,SecurityContextHolder 会使用 ThreadLocal 来存储这些信息,意味着安全上下文始终可用于同一执行线程中的方法。

获取有关当前用户的信息

因为身份信息与线程是绑定的,所以可以在程序的任何地方使用静态方法获取用户信息。例如获取当前经过身份验证的用户的名称,代码如下:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {String username = ((UserDetails)principal).getUsername();
} else {String username = principal.toString();
}

其中,getAuthentication() 返回认证信息,getPrincipal() 返回身份信息,UserDetails 是对用户信息的封装类。

Authentication

认证信息接口,集成了 Principal 类。该接口中方法如下:

接口方法 功能说明
getAuthorities() 获取权限信息列表,默认是 GrantedAuthority 接口的一些实现类,通常是代表权限信息的一系列字符串
getCredentials() 获取用户提交的密码凭证,用户输入的密码字符窜,在认证过后通常会被移除,用于保障安全
getDetails() 获取用户详细信息,用于记录 ip、sessionid、证书序列号等值
getPrincipal() 获取用户身份信息,大部分情况下返回的是 UserDetails 接口的实现类,是框架中最常用的接口之一

AuthenticationManager

认证管理器,负责验证。认证成功后,AuthenticationManager 返回一个填充了用户认证信息(包括权限信息、身份信息、详细信息等,但密码通常会被移除)的 Authentication 实例。然后再将 Authentication 设置到 SecurityContextHolder 容器中。

AuthenticationManager 接口是认证相关的核心接口,也是发起认证的入口。但它一般不直接认证,其常用实现类 ProviderManager 内部会维护一个 List<AuthenticationProvider> 列表,存放里多种认证方式,默认情况下,只需要通过一个 AuthenticationProvider 的认证,就可被认为是登录成功。

UserDetailsService

负责从特定的地方加载用户信息,通常是通过JdbcDaoImpl从数据库加载实现,也可以通过内存映射InMemoryDaoImpl实现。

UserDetails

该接口代表了最详细的用户信息。该接口中方法如下:

接口方法 功能说明
getAuthorities() 获取授予用户的权限
getPassword() 获取用户正确的密码,这个密码在验证时会和 Authentication 中的 getCredentials() 做比对
getUsername() 获取用于验证的用户名
isAccountNonExpired() 指示用户的帐户是否已过期,无法验证过期的用户
isAccountNonLocked() 指示用户的账号是否被锁定,无法验证被锁定的用户
isCredentialsNonExpired() 指示用户的凭据(密码)是否已过期,无法验证凭证过期的用户
isEnabled() 指示用户是否被启用,无法验证被禁用的用户

Spring Security 实战

1.系统设计

本文主要使用 Spring Security 来实现系统页面的权限控制和安全认证,本示例不做详细的数据增删改查,sql 可以在完整代码里下载,主要是基于数据库对页面 和 ajax 请求做权限控制。

1.1 技术栈

  • 编程语言:Java
  • 编程框架:Spring、Spring MVC、Spring Boot
  • ORM 框架:MyBatis
  • 视图模板引擎:Thymeleaf
  • 安全框架:Spring Security(5.2.0)
  • 数据库:MySQL
  • 前端:Layui、JQuery

1.2 功能设计

  1. 实现登录、退出
  2. 实现菜单 url 跳转的权限控制
  3. 实现按钮 ajax 请求的权限控制
  4. 防止跨站请求伪造(CSRF)攻击

1.3 数据库层设计

t_user 用户表

字段 类型 长度 是否为空 说明
id int 8 主键,自增长
username varchar 20 用户名
password varchar 255 密码

t_role 角色表

字段 类型 长度 是否为空 说明
id int 8 主键,自增长
role_name varchar 20 角色名称

t_menu 菜单表

字段 类型 长度 是否为空 说明
id int 8 主键,自增长
menu_name varchar 20 菜单名称
menu_url varchar 50 菜单url(Controller 请求路径)

t_user_roles 用户权限表

字段 类型 长度 是否为空 说明
id int 8 主键,自增长
user_id int 8 用户表id
role_id int 8 角色表id

t_role_menus 权限菜单表

字段 类型 长度 是否为空 说明
id int 8 主键,自增长
role_id int 8 角色表id
menu_id int 8 菜单表id

实体类这里不详细列了。

2.代码实现

2.0 相关依赖

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><!-- 热部署模块 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><optional>true</optional> <!-- 这个需要为 true 热部署才有效 --></dependency><!-- mysql 数据库驱动. --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!-- mybaits --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.0</version></dependency><!-- thymeleaf --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><!-- alibaba fastjson --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.47</version></dependency><!-- spring security --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency></dependencies>

2.1 继承 WebSecurityConfigurerAdapter 自定义 Spring Security 配置

/**
prePostEnabled :决定Spring Security的前注解是否可用 [@PreAuthorize,@PostAuthorize,..]
secureEnabled : 决定是否Spring Security的保障注解 [@Secured] 是否可用
jsr250Enabled :决定 JSR-250 annotations 注解[@RolesAllowed..] 是否可用.*/
@Configurable
@EnableWebSecurity
//开启 Spring Security 方法级安全注解 @EnableGlobalMethodSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{@Autowiredprivate CustomAccessDeniedHandler customAccessDeniedHandler;@Autowiredprivate UserDetailsService userDetailsService;/*** 静态资源设置*/@Overridepublic void configure(WebSecurity webSecurity) {//不拦截静态资源,所有用户均可访问的资源webSecurity.ignoring().antMatchers("/","/css/**","/js/**","/images/**","/layui/**");}/*** http请求设置*/@Overridepublic void configure(HttpSecurity http) throws Exception {//http.csrf().disable(); //注释就是使用 csrf 功能       http.headers().frameOptions().disable();//解决 in a frame because it set 'X-Frame-Options' to 'DENY' 问题           //http.anonymous().disable();http.authorizeRequests().antMatchers("/login/**","/initUserData","/main")//不拦截登录相关方法        .permitAll()        //.antMatchers("/user").hasRole("ADMIN")  // user接口只有ADMIN角色的可以访问
//          .anyRequest()
//          .authenticated()// 任何尚未匹配的URL只需要验证用户即可访问.anyRequest().access("@rbacPermission.hasPermission(request, authentication)")//根据账号权限访问         .and().formLogin().loginPage("/").loginPage("/login")   //登录请求页.loginProcessingUrl("/login")  //登录POST请求路径.usernameParameter("username") //登录用户名参数.passwordParameter("password") //登录密码参数.defaultSuccessUrl("/main")   //默认登录成功页面.and().exceptionHandling().accessDeniedHandler(customAccessDeniedHandler) //无权限处理器.and().logout().logoutSuccessUrl("/login?logout");  //退出登录成功URL}/*** 自定义获取用户信息接口*/@Overridepublic void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());}/*** 密码加密算法* @return*/@Beanpublic BCryptPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}

2.2 自定义实现 UserDetails 接口,扩展属性

public class UserEntity implements UserDetails {/*** */private static final long serialVersionUID = -9005214545793249372L;private Long id;// 用户idprivate String username;// 用户名private String password;// 密码private List<Role> userRoles;// 用户权限集合private List<Menu> roleMenus;// 角色菜单集合private Collection<? extends GrantedAuthority> authorities;public UserEntity() {}public UserEntity(String username, String password, Collection<? extends GrantedAuthority> authorities,List<Menu> roleMenus) {this.username = username;this.password = password;this.authorities = authorities;this.roleMenus = roleMenus;}public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}public List<Role> getUserRoles() {return userRoles;}public void setUserRoles(List<Role> userRoles) {this.userRoles = userRoles;}public List<Menu> getRoleMenus() {return roleMenus;}public void setRoleMenus(List<Menu> roleMenus) {this.roleMenus = roleMenus;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return this.authorities;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}}

2.3 自定义实现 UserDetailsService 接口

/*** 获取用户相关信息* @author charlie**/
@Service
public class UserDetailServiceImpl implements UserDetailsService {private Logger log = LoggerFactory.getLogger(UserDetailServiceImpl.class);@Autowiredprivate UserDao userDao;@Autowiredprivate RoleDao roleDao;@Autowiredprivate MenuDao menuDao;@Overridepublic UserEntity loadUserByUsername(String username) throws UsernameNotFoundException {// 根据用户名查找用户UserEntity user = userDao.getUserByUsername(username);System.out.println(user);if (user != null) {System.out.println("UserDetailsService");//根据用户id获取用户角色List<Role> roles = roleDao.getUserRoleByUserId(user.getId());// 填充权限Collection<SimpleGrantedAuthority> authorities = new HashSet<SimpleGrantedAuthority>();for (Role role : roles) {authorities.add(new SimpleGrantedAuthority(role.getRoleName()));}//填充权限菜单List<Menu> menus=menuDao.getRoleMenuByRoles(roles);return new UserEntity(username,user.getPassword(),authorities,menus);} else {System.out.println(username +" not found");throw new UsernameNotFoundException(username +" not found");}       }}

2.4 自定义实现 URL 权限控制

/*** RBAC数据模型控制权限* @author charlie**/
@Component("rbacPermission")
public class RbacPermission{private AntPathMatcher antPathMatcher = new AntPathMatcher();public boolean hasPermission(HttpServletRequest request, Authentication authentication) {Object principal = authentication.getPrincipal();boolean hasPermission = false;// 读取用户所拥有的权限菜单List<Menu> menus = ((UserEntity) principal).getRoleMenus();System.out.println(menus.size());for (Menu menu : menus) {if (antPathMatcher.match(menu.getMenuUrl(), request.getRequestURI())) {hasPermission = true;break;}}return hasPermission;}
}

2.5 实现 AccessDeniedHandler

自定义处理无权请求

/*** 处理无权请求* @author charlie**/
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {private Logger log = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response,AccessDeniedException accessDeniedException) throws IOException, ServletException {boolean isAjax = ControllerTools.isAjaxRequest(request);System.out.println("CustomAccessDeniedHandler handle");if (!response.isCommitted()) {if (isAjax) {String msg = accessDeniedException.getMessage();log.info("accessDeniedException.message:" + msg);String accessDenyMsg = "{\"code\":\"403\",\"msg\":\"没有权限\"}";ControllerTools.print(response, accessDenyMsg);} else {request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException);response.setStatus(HttpStatus.FORBIDDEN.value());RequestDispatcher dispatcher = request.getRequestDispatcher("/403");dispatcher.forward(request, response);}}}public static class ControllerTools {public static boolean isAjaxRequest(HttpServletRequest request) {return "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));}public static void print(HttpServletResponse response, String msg) throws IOException {response.setCharacterEncoding("UTF-8");response.setContentType("application/json; charset=utf-8");PrintWriter writer = response.getWriter();writer.write(msg);writer.flush();writer.close();}}}

2.6 相关 Controller

登录/退出跳转

/*** 登录/退出跳转* @author charlie**/
@Controller
public class LoginController {@GetMapping("/login")public ModelAndView login(@RequestParam(value = "error", required = false) String error,@RequestParam(value = "logout", required = false) String logout) {ModelAndView mav = new ModelAndView();if (error != null) {mav.addObject("error", "用户名或者密码不正确");}if (logout != null) {mav.addObject("msg", "退出成功");}mav.setViewName("login");return mav;}
}

登录成功跳转

@Controller
public class MainController {@GetMapping("/main")public ModelAndView toMainPage() {//获取登录的用户名Object principal= SecurityContextHolder.getContext().getAuthentication().getPrincipal();String username=null;if(principal instanceof UserDetails) {username=((UserDetails)principal).getUsername();}else {username=principal.toString();}ModelAndView mav = new ModelAndView();mav.setViewName("main");mav.addObject("username", username);return mav;}}

用于不同权限页面访问测试

/*** 用于不同权限页面访问测试* @author charlie**/
@Controller
public class ResourceController {@GetMapping("/publicResource")public String toPublicResource() {return "resource/public";}@GetMapping("/vipResource")public String toVipResource() {return "resource/vip";}
}

用于不同权限ajax请求测试

/*** 用于不同权限ajax请求测试* @author charlie**/
@RestController
@RequestMapping("/test")
public class HttptestController {@PostMapping("/public")public JSONObject doPublicHandler(Long id) {JSONObject json = new JSONObject();json.put("code", 200);json.put("msg", "请求成功" + id);return json;}@PostMapping("/vip")public JSONObject doVipHandler(Long id) {JSONObject json = new JSONObject();json.put("code", 200);json.put("msg", "请求成功" + id);return json;}
}

2.7 相关 html 页面

登录页面

<form class="layui-form" action="/login" method="post"><div class="layui-input-inline"><input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/><input type="text" name="username" requiredplaceholder="用户名" autocomplete="off" class="layui-input"></div><div class="layui-input-inline"><input type="password" name="password" required  placeholder="密码" autocomplete="off"class="layui-input"></div><div class="layui-input-inline login-btn"><button id="btnLogin" lay-submit lay-filter="*" class="layui-btn">登录</button></div><div class="form-message"><label th:text="${error}"></label><label th:text="${msg}"></label></div></form>

防止跨站请求伪造(CSRF)攻击

退出系统

<form id="logoutForm" action="/logout" method="post"style="display: none;"><input type="hidden" th:name="${_csrf.parameterName}"th:value="${_csrf.token}"></form><ahref="javascript:document.getElementById('logoutForm').submit();">退出系统</a>

ajax 请求页面

<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" id="hidCSRF">
<button class="layui-btn" id="btnPublic">公共权限请求按钮</button>
<br>
<br>
<button class="layui-btn" id="btnVip">VIP权限请求按钮</button>
<script type="text/javascript" th:src="@{/js/jquery-1.8.3.min.js}"></script>
<script type="text/javascript" th:src="@{/layui/layui.js}"></script>
<script type="text/javascript">layui.use('form', function() {var form = layui.form;$("#btnPublic").click(function(){$.ajax({url:"/test/public",type:"POST",data:{id:1},beforeSend:function(xhr){xhr.setRequestHeader('X-CSRF-TOKEN',$("#hidCSRF").val());   },success:function(res){alert(res.code+":"+res.msg);}   });});$("#btnVip").click(function(){$.ajax({url:"/test/vip",type:"POST",data:{id:2},beforeSend:function(xhr){xhr.setRequestHeader('X-CSRF-TOKEN',$("#hidCSRF").val());   },success:function(res){alert(res.code+":"+res.msg);}});});});</script>

2.8 测试

测试提供两个账号:user 和 admin (密码与账号一样)

由于 admin 作为管理员权限,设置了全部的访问权限,这里只展示 user 的测试结果。



完整代码

github

码云

非特殊说明,本文版权归 朝雾轻寒 所有,转载请注明出处.

原文标题:Spring Boot 2.X(十八):集成 Spring Security-登录认证和权限控制

原文地址:https://www.zwqh.top/article/info/27

如果文章有不足的地方,欢迎提点,后续会完善。

  相关解决方案