SpringBoot整合Spring Security

向右看齐 2024-04-18 08:34 146阅读 0赞

最近研究了一下Spring Security记录一下。

Spring Security简介

Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。

创建demo项目

项目已传GitHub,有需要的小伙伴可以下载传送门

新建一个springboot项目
pom文件内容

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  4. <modelVersion>4.0.0</modelVersion>
  5. <parent>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-parent</artifactId>
  8. <version>2.1.7.RELEASE</version>
  9. <relativePath/> <!-- lookup parent from repository -->
  10. </parent>
  11. <groupId>cn.duyunzhi</groupId>
  12. <artifactId>security</artifactId>
  13. <version>1.0.0</version>
  14. <name>security</name>
  15. <description>security project for Spring Boot</description>
  16. <properties>
  17. <java.version>1.8</java.version>
  18. </properties>
  19. <dependencies>
  20. <!--security-->
  21. <dependency>
  22. <groupId>org.springframework.boot</groupId>
  23. <artifactId>spring-boot-starter-security</artifactId>
  24. </dependency>
  25. <dependency>
  26. <groupId>org.springframework.boot</groupId>
  27. <artifactId>spring-boot-starter-web</artifactId>
  28. </dependency>
  29. <!--thymeleaf模板-->
  30. <dependency>
  31. <groupId>org.springframework.boot</groupId>
  32. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  33. </dependency>
  34. <!--test-->
  35. <dependency>
  36. <groupId>org.springframework.boot</groupId>
  37. <artifactId>spring-boot-starter-test</artifactId>
  38. <scope>test</scope>
  39. </dependency>
  40. <dependency>
  41. <groupId>org.springframework.security</groupId>
  42. <artifactId>spring-security-test</artifactId>
  43. <scope>test</scope>
  44. </dependency>
  45. <!--mysql驱动-->
  46. <dependency>
  47. <groupId>mysql</groupId>
  48. <artifactId>mysql-connector-java</artifactId>
  49. <scope>runtime</scope>
  50. </dependency>
  51. <!--lombok-->
  52. <dependency>
  53. <groupId>org.projectlombok</groupId>
  54. <artifactId>lombok</artifactId>
  55. </dependency>
  56. <!--mybatis-plus-->
  57. <dependency>
  58. <groupId>com.baomidou</groupId>
  59. <artifactId>mybatis-plus-boot-starter</artifactId>
  60. <version>3.1.0</version>
  61. </dependency>
  62. <!--Druid连接池包 -->
  63. <dependency>
  64. <groupId>com.alibaba</groupId>
  65. <artifactId>druid</artifactId>
  66. <version>1.0.12</version>
  67. </dependency>
  68. <!--日志依赖-->
  69. <dependency>
  70. <groupId>org.slf4j</groupId>
  71. <artifactId>log4j-over-slf4j</artifactId>
  72. </dependency>
  73. </dependencies>
  74. <build>
  75. <plugins>
  76. <plugin>
  77. <groupId>org.springframework.boot</groupId>
  78. <artifactId>spring-boot-maven-plugin</artifactId>
  79. </plugin>
  80. </plugins>
  81. </build>
  82. </project>

Security配置

  1. import cn.duyunzhi.security.service.CustomUserDetailsService;
  2. import cn.duyunzhi.security.common.Md5PasswordEncoder;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.context.annotation.Configuration;
  6. import org.springframework.security.authentication.AuthenticationManager;
  7. import org.springframework.security.config.BeanIds;
  8. import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
  9. import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
  10. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  11. import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
  12. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  13. import org.springframework.web.cors.CorsConfiguration;
  14. import org.springframework.web.cors.CorsConfigurationSource;
  15. import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
  16. import java.util.Arrays;
  17. /**
  18. * @Author: Ezreal-d
  19. * @Description:
  20. * @Date: 2019/9/2 11:28
  21. */
  22. @Configuration
  23. @EnableWebSecurity
  24. @EnableGlobalMethodSecurity(prePostEnabled = true)
  25. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  26. @Autowired
  27. private CustomUserDetailsService customUserDetailsService;
  28. @Override
  29. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  30. auth.userDetailsService(customUserDetailsService())
  31. .passwordEncoder(passwordEncoder());
  32. }
  33. @Override
  34. protected void configure(HttpSecurity http) throws Exception {
  35. http.authorizeRequests()
  36. // 所有用户均可访问的资源 只支持GET请求,其它请求需做处理
  37. .antMatchers("/favicon.ico", "/css/**", "/js/**", "/img/**", "/login/toLogin", "/login/getVerify", "/login/userLogin").permitAll()
  38. // 任何尚未匹配的URL只需要验证用户即可访问
  39. .anyRequest().authenticated()
  40. .and()
  41. .formLogin().loginPage("/login/toLogin").successForwardUrl("/index")
  42. .and()
  43. //权限拒绝的页面
  44. .exceptionHandling().accessDeniedPage("/error-403");
  45. http.logout().logoutSuccessUrl("/login/toLogin");
  46. }
  47. /**
  48. * 设置用户密码的加密方式
  49. *
  50. * @return
  51. */
  52. @Bean
  53. public Md5PasswordEncoder passwordEncoder() {
  54. return new Md5PasswordEncoder();
  55. }
  56. /**
  57. * 自定义UserDetailsService,授权
  58. *
  59. * @return
  60. */
  61. private CustomUserDetailsService customUserDetailsService() {
  62. return customUserDetailsService;
  63. }
  64. /**
  65. * AuthenticationManager
  66. *
  67. * @return
  68. * @throws Exception
  69. */
  70. @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
  71. @Override
  72. public AuthenticationManager authenticationManagerBean() throws Exception {
  73. return super.authenticationManagerBean();
  74. }
  75. }

Md5PasswordEncoder.java

  1. import cn.duyunzhi.security.utils.CipherUtil;
  2. import org.springframework.security.crypto.password.PasswordEncoder;
  3. /**
  4. * @Author: Ezreal-d
  5. * @Description: 采用MD5加密密码
  6. * @Date: 2019/9/2 15:38
  7. */
  8. public class Md5PasswordEncoder implements PasswordEncoder {
  9. @Override
  10. public String encode(CharSequence rawPassword) {
  11. return CipherUtil.generatePassword(rawPassword.toString());
  12. }
  13. @Override
  14. public boolean matches(CharSequence rawPassword, String encodedPassword) {
  15. return CipherUtil.generatePassword(rawPassword.toString()).equals(encodedPassword);
  16. }
  17. }

所有用户均可访问的资源配置项里面路径如果不是GET请求需要做另外处理。原因是:

Spring Security 4.0之后,引入了CSRF,默认是开启。不得不说,CSRF和RESTful技术有冲突。CSRF默认支持的方法: GET|HEAD|TRACE|OPTIONS,不支持POST。

所以问题来了,我这边用户登录用的就是POST请求,结果发现403错误。疯狂采坑!
解决方案

  • 禁用csrf
    在SecurityConfig 中添加

    http.csrf().disable();

  • 携带Token
    在发送ajax请求的时候带上一个 X-CSRF-TOKEN的请求头。我用的是thymeleaf模板,如下:
    在head标签中添加

然后加了一个common.js

  1. $(function () {
  2. var token = $("meta[name='_csrf']").attr("content");
  3. var header = $("meta[name='_csrf_header']").attr("content");
  4. $(document).ajaxSend(function(e, xhr, options) {
  5. xhr.setRequestHeader(header, token);
  6. });
  7. });

再次发送POST请求的时候就OK了!

认证和授权

  1. import cn.duyunzhi.security.entity.Role;
  2. import cn.duyunzhi.security.entity.User;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.security.core.GrantedAuthority;
  5. import org.springframework.security.core.authority.SimpleGrantedAuthority;
  6. import org.springframework.security.core.userdetails.UserDetails;
  7. import org.springframework.security.core.userdetails.UserDetailsService;
  8. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  9. import org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority;
  10. import org.springframework.stereotype.Service;
  11. import java.util.ArrayList;
  12. import java.util.List;
  13. /**
  14. * @Author: Ezreal-d
  15. * @Description: 认证和授权
  16. * @Date: 2019/9/2 11:32
  17. */
  18. @Service
  19. public class CustomUserDetailsService implements UserDetailsService {
  20. @Autowired
  21. private UserService userService;
  22. @Autowired
  23. private RoleService roleService;
  24. @Override
  25. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  26. //认证账号
  27. User user = userService.loadUserByUsername(username);
  28. if (user == null) {
  29. throw new UsernameNotFoundException("The account does not exist");
  30. }
  31. //开始授权
  32. Role role = roleService.getRoleByRoleId(user.getRole());
  33. List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
  34. GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getRoleName());
  35. //此处将权限信息添加到 GrantedAuthority 对象中,在后面进行全权限验证时会使
  36. //GrantedAuthority 对象。
  37. grantedAuthorities.add(grantedAuthority);
  38. user.setAuthorities(grantedAuthorities);
  39. return user;
  40. }
  41. }

我这边数据库用户和角色是一对第一的,一个用户只有一个角色。多个角色这边也可以授权多个。

用户登录

  1. import cn.duyunzhi.security.bean.JsonAsynResult;
  2. import cn.duyunzhi.security.entity.User;
  3. import cn.duyunzhi.security.service.UserService;
  4. import cn.duyunzhi.security.utils.DateUtil;
  5. import cn.duyunzhi.security.utils.RandomValidateCodeUtil;
  6. import cn.duyunzhi.security.utils.SecurityUtil;
  7. import org.slf4j.Logger;
  8. import org.slf4j.LoggerFactory;
  9. import org.springframework.beans.factory.annotation.Autowired;
  10. import org.springframework.security.authentication.AuthenticationManager;
  11. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  12. import org.springframework.security.core.Authentication;
  13. import org.springframework.security.core.context.SecurityContextHolder;
  14. import org.springframework.stereotype.Controller;
  15. import org.springframework.util.StringUtils;
  16. import org.springframework.web.bind.annotation.*;
  17. import javax.servlet.http.HttpServletRequest;
  18. import javax.servlet.http.HttpServletResponse;
  19. import javax.servlet.http.HttpSession;
  20. import java.time.LocalDateTime;
  21. /**
  22. * @Author: Ezreal-d
  23. * @Description: 登录Controller
  24. * @Date: 2019/9/2 15:39
  25. */
  26. @Controller
  27. @RequestMapping("/login")
  28. public class LoginController extends BaseController {
  29. private static final Logger logger = LoggerFactory.getLogger(LoginController.class);
  30. @Autowired
  31. private AuthenticationManager myAuthenticationManager;
  32. @Autowired
  33. private UserService userService;
  34. @RequestMapping("/toLogin")
  35. public String login() {
  36. return "login";
  37. }
  38. @PostMapping("/userLogin")
  39. @ResponseBody
  40. public JsonAsynResult userLogin(User user, String inputStr, HttpSession session) {
  41. if (StringUtils.isEmpty(inputStr) || !((String) session.getAttribute(RandomValidateCodeUtil.RANDOMCODEKEY)).toLowerCase().equals(inputStr.toLowerCase())) {
  42. return JsonAsynResult.createErrorResult("验证码错误");
  43. }
  44. UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
  45. //使用SpringSecurity拦截登陆请求 进行认证和授权
  46. Authentication authenticate = myAuthenticationManager.authenticate(authenticationToken);
  47. SecurityContextHolder.getContext().setAuthentication(authenticate);
  48. // 这个非常重要,否则验证后将无法登陆
  49. session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());
  50. //更新用户登录信息
  51. User updateUser = SecurityUtil.getUser();
  52. updateUser.setLastLoginTime(DateUtil.getNowDateTime());
  53. updateUser.setLastLoginIp(getUserIp());
  54. userService.updateUser(updateUser);
  55. return JsonAsynResult.createSucResult();
  56. }
  57. /**
  58. * 生成验证码
  59. */
  60. @GetMapping(value = "/getVerify")
  61. public void getVerify(HttpServletRequest request, HttpServletResponse response) {
  62. try {
  63. //设置相应类型,告诉浏览器输出的内容为图片
  64. response.setContentType("image/jpeg");
  65. //设置响应头信息,告诉浏览器不要缓存此内容
  66. response.setHeader("Pragma", "No-cache");
  67. response.setHeader("Cache-Control", "no-cache");
  68. response.setDateHeader("Expire", 0);
  69. RandomValidateCodeUtil randomValidateCode = new RandomValidateCodeUtil();
  70. //输出验证码图片方法
  71. randomValidateCode.getRandcode(request, response);
  72. } catch (Exception e) {
  73. logger.error("Failed to get verification code >>>> ", e);
  74. }
  75. }
  76. @RequestMapping("/logout")
  77. public String logout() {
  78. SecurityUtil.logout();
  79. return "redirect:login/toLogin";
  80. }
  81. }

贴一下工具类

  1. import org.slf4j.Logger;
  2. import org.slf4j.LoggerFactory;
  3. import javax.imageio.ImageIO;
  4. import javax.servlet.http.HttpServletRequest;
  5. import javax.servlet.http.HttpServletResponse;
  6. import javax.servlet.http.HttpSession;
  7. import java.awt.*;
  8. import java.awt.image.BufferedImage;
  9. import java.util.Random;
  10. /**
  11. * @Author: Ezreal-d
  12. * @Description: 验证码工具类
  13. * @Date: 2019/9/2 16:44
  14. */
  15. public class RandomValidateCodeUtil {
  16. /**
  17. * 放到session中的key
  18. */
  19. public static final String RANDOMCODEKEY = "RANDOMVALIDATECODEKEY";
  20. /**
  21. * 随机产生数字与字母组合的字符串
  22. */
  23. private String randString = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  24. /**
  25. * 图片宽
  26. */
  27. private final int width = 93;
  28. /**
  29. * 图片高
  30. */
  31. private final int height = 41;
  32. /**
  33. * 干扰线数量
  34. */
  35. private final int lineSize = 40;
  36. /**
  37. * 随机产生字符数量
  38. */
  39. private final int stringNum = 4;
  40. private static final Logger logger = LoggerFactory.getLogger(RandomValidateCodeUtil.class);
  41. private Random random = new Random();
  42. /**
  43. * 获得字体
  44. */
  45. private Font getFont() {
  46. return new Font("Fixedsys", Font.BOLD, 18);
  47. }
  48. /**
  49. * 获得颜色
  50. */
  51. private Color getRandColor(int fc, int bc) {
  52. if (fc > 255) {
  53. fc = 255;
  54. }
  55. if (bc > 255) {
  56. bc = 255;
  57. }
  58. int r = fc + random.nextInt(bc - fc - 16);
  59. int g = fc + random.nextInt(bc - fc - 14);
  60. int b = fc + random.nextInt(bc - fc - 18);
  61. return new Color(r, g, b);
  62. }
  63. /**
  64. * 生成随机图片
  65. */
  66. public void getRandcode(HttpServletRequest request, HttpServletResponse response) {
  67. HttpSession session = request.getSession();
  68. // BufferedImage类是具有缓冲区的Image类,Image类是用于描述图像信息的类
  69. BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
  70. // 产生Image对象的Graphics对象,改对象可以在图像上进行各种绘制操作
  71. Graphics g = image.getGraphics();
  72. //图片大小
  73. g.fillRect(0, 0, width, height);
  74. //字体大小
  75. g.setFont(new Font("Times New Roman", Font.PLAIN, 18));
  76. //字体颜色
  77. g.setColor(getRandColor(110, 133));
  78. // 绘制干扰线
  79. for (int i = 0; i <= lineSize; i++) {
  80. drowLine(g);
  81. }
  82. // 绘制随机字符
  83. String randomString = "";
  84. for (int i = 1; i <= stringNum; i++) {
  85. randomString = drowString(g, randomString, i);
  86. }
  87. logger.info(randomString);
  88. //将生成的随机字符串保存到session中
  89. session.removeAttribute(RANDOMCODEKEY);
  90. session.setAttribute(RANDOMCODEKEY, randomString);
  91. g.dispose();
  92. try {
  93. // 将内存中的图片通过流动形式输出到客户端
  94. ImageIO.write(image, "JPEG", response.getOutputStream());
  95. } catch (Exception e) {
  96. logger.error("将内存中的图片通过流动形式输出到客户端失败>>>> ", e);
  97. }
  98. }
  99. /**
  100. * 绘制字符串
  101. */
  102. private String drowString(Graphics g, String randomString, int i) {
  103. g.setFont(getFont());
  104. g.setColor(new Color(random.nextInt(101), random.nextInt(111), random
  105. .nextInt(121)));
  106. String rand = String.valueOf(getRandomString(random.nextInt(randString
  107. .length())));
  108. randomString += rand;
  109. g.translate(random.nextInt(3), random.nextInt(3));
  110. g.drawString(rand, 13 * i, 16);
  111. return randomString;
  112. }
  113. /**
  114. * 绘制干扰线
  115. */
  116. private void drowLine(Graphics g) {
  117. int x = random.nextInt(width);
  118. int y = random.nextInt(height);
  119. int xl = random.nextInt(13);
  120. int yl = random.nextInt(15);
  121. g.drawLine(x, y, x + xl, y + yl);
  122. }
  123. /**
  124. * 获取随机的字符
  125. */
  126. public String getRandomString(int num) {
  127. return String.valueOf(randString.charAt(num));
  128. }
  129. }
  130. import cn.duyunzhi.security.entity.User;
  131. import org.springframework.security.core.Authentication;
  132. import org.springframework.security.core.GrantedAuthority;
  133. import org.springframework.security.core.context.SecurityContextHolder;
  134. import org.springframework.util.StringUtils;
  135. import java.util.Collection;
  136. /**
  137. * @Author: Ezreal-d
  138. * @Description: Security工具类
  139. * @Date: 2019/9/3 13:11
  140. */
  141. public class SecurityUtil {
  142. /**
  143. * 获取认证信息
  144. * @return
  145. */
  146. public static Authentication getAuthentication() {
  147. return SecurityContextHolder.getContext().getAuthentication();
  148. }
  149. /**
  150. * 获取所有权限
  151. * @return
  152. */
  153. public static Collection<? extends GrantedAuthority> getAllPermission(){
  154. return getAuthentication().getAuthorities();
  155. }
  156. /**
  157. * 是否有权限
  158. * @param permission
  159. * @return
  160. */
  161. public static boolean hasPermission(String permission){
  162. if(StringUtils.isEmpty(permission)){
  163. return false;
  164. }
  165. Collection<? extends GrantedAuthority> authorities = getAllPermission();
  166. boolean hasPermission = false;
  167. for(GrantedAuthority grantedAuthority : authorities){
  168. String authority = grantedAuthority.getAuthority();
  169. if(authority.equals(permission)){
  170. hasPermission =true;
  171. }
  172. }
  173. return hasPermission;
  174. }
  175. /**
  176. * 获取登录用户
  177. * @return
  178. */
  179. public static User getUser() {
  180. return (User) getAuthentication().getPrincipal();
  181. }
  182. /**
  183. * 注销
  184. */
  185. public static void logout(){
  186. SecurityContextHolder.clearContext();
  187. }
  188. }

发表评论

表情:
评论列表 (有 0 条评论,146人围观)

还没有评论,来说两句吧...

相关阅读

    相关 springboot整合spring-security

    在web开发中,安全性问题比较重要,一般会使用过滤器或者拦截器的方式对权限等进行验证过滤。此博客根据b站up主,使用demo示例进行展示spring-security的一些功能