SpringBoot整合Spring Security
最近研究了一下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文件内容
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.duyunzhi</groupId>
<artifactId>security</artifactId>
<version>1.0.0</version>
<name>security</name>
<description>security project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--thymeleaf模板-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<!--Druid连接池包 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.12</version>
</dependency>
<!--日志依赖-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Security配置
import cn.duyunzhi.security.service.CustomUserDetailsService;
import cn.duyunzhi.security.common.Md5PasswordEncoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
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.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
/**
* @Author: Ezreal-d
* @Description:
* @Date: 2019/9/2 11:28
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService())
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 所有用户均可访问的资源 只支持GET请求,其它请求需做处理
.antMatchers("/favicon.ico", "/css/**", "/js/**", "/img/**", "/login/toLogin", "/login/getVerify", "/login/userLogin").permitAll()
// 任何尚未匹配的URL只需要验证用户即可访问
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login/toLogin").successForwardUrl("/index")
.and()
//权限拒绝的页面
.exceptionHandling().accessDeniedPage("/error-403");
http.logout().logoutSuccessUrl("/login/toLogin");
}
/**
* 设置用户密码的加密方式
*
* @return
*/
@Bean
public Md5PasswordEncoder passwordEncoder() {
return new Md5PasswordEncoder();
}
/**
* 自定义UserDetailsService,授权
*
* @return
*/
private CustomUserDetailsService customUserDetailsService() {
return customUserDetailsService;
}
/**
* AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
Md5PasswordEncoder.java
import cn.duyunzhi.security.utils.CipherUtil;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @Author: Ezreal-d
* @Description: 采用MD5加密密码
* @Date: 2019/9/2 15:38
*/
public class Md5PasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
return CipherUtil.generatePassword(rawPassword.toString());
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return CipherUtil.generatePassword(rawPassword.toString()).equals(encodedPassword);
}
}
所有用户均可访问的资源配置项里面路径如果不是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
$(function () {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function(e, xhr, options) {
xhr.setRequestHeader(header, token);
});
});
再次发送POST请求的时候就OK了!
认证和授权
import cn.duyunzhi.security.entity.Role;
import cn.duyunzhi.security.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: Ezreal-d
* @Description: 认证和授权
* @Date: 2019/9/2 11:32
*/
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//认证账号
User user = userService.loadUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("The account does not exist");
}
//开始授权
Role role = roleService.getRoleByRoleId(user.getRole());
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getRoleName());
//此处将权限信息添加到 GrantedAuthority 对象中,在后面进行全权限验证时会使
//GrantedAuthority 对象。
grantedAuthorities.add(grantedAuthority);
user.setAuthorities(grantedAuthorities);
return user;
}
}
我这边数据库用户和角色是一对第一的,一个用户只有一个角色。多个角色这边也可以授权多个。
用户登录
import cn.duyunzhi.security.bean.JsonAsynResult;
import cn.duyunzhi.security.entity.User;
import cn.duyunzhi.security.service.UserService;
import cn.duyunzhi.security.utils.DateUtil;
import cn.duyunzhi.security.utils.RandomValidateCodeUtil;
import cn.duyunzhi.security.utils.SecurityUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.time.LocalDateTime;
/**
* @Author: Ezreal-d
* @Description: 登录Controller
* @Date: 2019/9/2 15:39
*/
@Controller
@RequestMapping("/login")
public class LoginController extends BaseController {
private static final Logger logger = LoggerFactory.getLogger(LoginController.class);
@Autowired
private AuthenticationManager myAuthenticationManager;
@Autowired
private UserService userService;
@RequestMapping("/toLogin")
public String login() {
return "login";
}
@PostMapping("/userLogin")
@ResponseBody
public JsonAsynResult userLogin(User user, String inputStr, HttpSession session) {
if (StringUtils.isEmpty(inputStr) || !((String) session.getAttribute(RandomValidateCodeUtil.RANDOMCODEKEY)).toLowerCase().equals(inputStr.toLowerCase())) {
return JsonAsynResult.createErrorResult("验证码错误");
}
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
//使用SpringSecurity拦截登陆请求 进行认证和授权
Authentication authenticate = myAuthenticationManager.authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authenticate);
// 这个非常重要,否则验证后将无法登陆
session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());
//更新用户登录信息
User updateUser = SecurityUtil.getUser();
updateUser.setLastLoginTime(DateUtil.getNowDateTime());
updateUser.setLastLoginIp(getUserIp());
userService.updateUser(updateUser);
return JsonAsynResult.createSucResult();
}
/**
* 生成验证码
*/
@GetMapping(value = "/getVerify")
public void getVerify(HttpServletRequest request, HttpServletResponse response) {
try {
//设置相应类型,告诉浏览器输出的内容为图片
response.setContentType("image/jpeg");
//设置响应头信息,告诉浏览器不要缓存此内容
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expire", 0);
RandomValidateCodeUtil randomValidateCode = new RandomValidateCodeUtil();
//输出验证码图片方法
randomValidateCode.getRandcode(request, response);
} catch (Exception e) {
logger.error("Failed to get verification code >>>> ", e);
}
}
@RequestMapping("/logout")
public String logout() {
SecurityUtil.logout();
return "redirect:login/toLogin";
}
}
贴一下工具类
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
/**
* @Author: Ezreal-d
* @Description: 验证码工具类
* @Date: 2019/9/2 16:44
*/
public class RandomValidateCodeUtil {
/**
* 放到session中的key
*/
public static final String RANDOMCODEKEY = "RANDOMVALIDATECODEKEY";
/**
* 随机产生数字与字母组合的字符串
*/
private String randString = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
/**
* 图片宽
*/
private final int width = 93;
/**
* 图片高
*/
private final int height = 41;
/**
* 干扰线数量
*/
private final int lineSize = 40;
/**
* 随机产生字符数量
*/
private final int stringNum = 4;
private static final Logger logger = LoggerFactory.getLogger(RandomValidateCodeUtil.class);
private Random random = new Random();
/**
* 获得字体
*/
private Font getFont() {
return new Font("Fixedsys", Font.BOLD, 18);
}
/**
* 获得颜色
*/
private Color getRandColor(int fc, int bc) {
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc - 16);
int g = fc + random.nextInt(bc - fc - 14);
int b = fc + random.nextInt(bc - fc - 18);
return new Color(r, g, b);
}
/**
* 生成随机图片
*/
public void getRandcode(HttpServletRequest request, HttpServletResponse response) {
HttpSession session = request.getSession();
// BufferedImage类是具有缓冲区的Image类,Image类是用于描述图像信息的类
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
// 产生Image对象的Graphics对象,改对象可以在图像上进行各种绘制操作
Graphics g = image.getGraphics();
//图片大小
g.fillRect(0, 0, width, height);
//字体大小
g.setFont(new Font("Times New Roman", Font.PLAIN, 18));
//字体颜色
g.setColor(getRandColor(110, 133));
// 绘制干扰线
for (int i = 0; i <= lineSize; i++) {
drowLine(g);
}
// 绘制随机字符
String randomString = "";
for (int i = 1; i <= stringNum; i++) {
randomString = drowString(g, randomString, i);
}
logger.info(randomString);
//将生成的随机字符串保存到session中
session.removeAttribute(RANDOMCODEKEY);
session.setAttribute(RANDOMCODEKEY, randomString);
g.dispose();
try {
// 将内存中的图片通过流动形式输出到客户端
ImageIO.write(image, "JPEG", response.getOutputStream());
} catch (Exception e) {
logger.error("将内存中的图片通过流动形式输出到客户端失败>>>> ", e);
}
}
/**
* 绘制字符串
*/
private String drowString(Graphics g, String randomString, int i) {
g.setFont(getFont());
g.setColor(new Color(random.nextInt(101), random.nextInt(111), random
.nextInt(121)));
String rand = String.valueOf(getRandomString(random.nextInt(randString
.length())));
randomString += rand;
g.translate(random.nextInt(3), random.nextInt(3));
g.drawString(rand, 13 * i, 16);
return randomString;
}
/**
* 绘制干扰线
*/
private void drowLine(Graphics g) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(13);
int yl = random.nextInt(15);
g.drawLine(x, y, x + xl, y + yl);
}
/**
* 获取随机的字符
*/
public String getRandomString(int num) {
return String.valueOf(randString.charAt(num));
}
}
import cn.duyunzhi.security.entity.User;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import java.util.Collection;
/**
* @Author: Ezreal-d
* @Description: Security工具类
* @Date: 2019/9/3 13:11
*/
public class SecurityUtil {
/**
* 获取认证信息
* @return
*/
public static Authentication getAuthentication() {
return SecurityContextHolder.getContext().getAuthentication();
}
/**
* 获取所有权限
* @return
*/
public static Collection<? extends GrantedAuthority> getAllPermission(){
return getAuthentication().getAuthorities();
}
/**
* 是否有权限
* @param permission
* @return
*/
public static boolean hasPermission(String permission){
if(StringUtils.isEmpty(permission)){
return false;
}
Collection<? extends GrantedAuthority> authorities = getAllPermission();
boolean hasPermission = false;
for(GrantedAuthority grantedAuthority : authorities){
String authority = grantedAuthority.getAuthority();
if(authority.equals(permission)){
hasPermission =true;
}
}
return hasPermission;
}
/**
* 获取登录用户
* @return
*/
public static User getUser() {
return (User) getAuthentication().getPrincipal();
}
/**
* 注销
*/
public static void logout(){
SecurityContextHolder.clearContext();
}
}
还没有评论,来说两句吧...