Spring Security是Spring官方推荐的认证、授权框架,功能相比Apache Shiro功能更丰富也更强大,但是使用起来更麻烦。
如果使用过Apache Shiro,学习Spring Security会比较简单一点,两种框架有很多相似的地方。
一、准备工作
1、创建项目
在IntelliJ IDEA中创建一个springboot项目springboot-springsecurity
2、添加依赖
修改pom.xml,添加项目所需的依赖。
<?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.5.9</version>
<relativePath />
</parent>
<groupId>cn.edu.sgu.www</groupId>
<artifactId>springboot-springsecurity</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-springsecurity</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<jjwt.version>0.9.1</jjwt.version>
<mysql.version>8.0.28</mysql.version>
<druid.version>1.1.21</druid.version>
<lombok.version>1.18.22</lombok.version>
<mybatis.version>2.2.2</mybatis.version>
<mybatis-plus.version>3.5.1</mybatis-plus.version>
</properties>
<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>
</dependency>
<!--validation-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--spring security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!--jjwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3、修改配置
将配置文件application.properties重命名为application.yml,修改配置文件的内容。
server:
port: 8080
servlet:
context-path: /
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/spring-security
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
mybatis-plus:
mapper-locations: classpath:mapper/*Mapper.xml
logging:
level:
springfox: error
cn.edu.sgu.www.security: debug
system:
login-page: /login.html
login-url: /user/login
index-page: /index.html
logout-url: /user/logout
parameter:
username: username
password: password
white-url:
- /js/**
- /css/**
- /images/**
- /user/login
- /login.html
二、创建相关的类
公共类
ResponseCode.java
响应状态码枚举类
package cn.edu.sgu.www.security.restful;
/**
* 响应状态码
* @author heyunlin
* @version 1.0
*/
public enum ResponseCode {
/**
* 请求成功
*/
OK(200),
/**
* 失败的请求
*/
BAD_REQUEST(400),
/**
* 未授权
*/
UNAUTHORIZED(401),
/**
* 禁止访问
*/
FORBIDDEN(403),
/**
* 找不到(该状态不可用)
*/
NOT_FOUND(404),
/**
* 不可访问
*/
NOT_ACCEPTABLE(406),
/**
* 冲突
*/
CONFLICT(409),
/**
* 服务器发生异常
*/
ERROR(500);
private final Integer value;
ResponseCode(Integer value) {
this.value = value;
}
public Integer getValue() {
return value;
}
}
GlobalException.java
自定义的全局异常类
package cn.edu.sgu.www.security.exception;
import cn.edu.sgu.www.security.restful.ResponseCode;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 自定义的全局异常类
* @author heyunlin
* @version 1.0
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class GlobalException extends RuntimeException {
private ResponseCode responseCode;
public GlobalException(ResponseCode responseCode, String message) {
super(message);
setResponseCode(responseCode);
}
}
JsonResult.java
自定义的统一响应实体类
package cn.edu.sgu.www.security.restful;
import lombok.Data;
/**
* 响应实体类
* @param <T> 响应数据的类型
* @author heyunlin
* @version 1.0
*/
@Data
public class JsonResult<T> {
/**
* 响应数据
*/
private T data;
/**
* 响应状态码
*/
private Integer code;
/**
* 响应提示信息
*/
private String message;
public static JsonResult<Void> success() {
return success(null);
}
public static JsonResult<Void> success(String message) {
return success(message, null);
}
public static <T> JsonResult<T> success(String message, T data) {
JsonResult<T> jsonResult = new JsonResult<>();
jsonResult.setCode(ResponseCode.OK.getValue());
jsonResult.setMessage(message);
jsonResult.setData(data);
return jsonResult;
}
public static JsonResult<Void> error(String message) {
JsonResult<Void> jsonResult = new JsonResult<>();
jsonResult.setCode(ResponseCode.ERROR.getValue());
jsonResult.setMessage(message);
return jsonResult;
}
public static JsonResult<Void> error(ResponseCode responseCode, Throwable e) {
return error(responseCode, e.getMessage() != null ? e.getMessage() : "系统发生异常,请联系管理员!");
}
public static JsonResult<Void> error(ResponseCode responseCode, String message) {
JsonResult<Void> jsonResult = new JsonResult<>();
jsonResult.setCode(responseCode.getValue());
jsonResult.setMessage(message);
return jsonResult;
}
}
GlobalExceptionHandler.java
全局异常处理类
package cn.edu.sgu.www.security.restful.handler;
import cn.edu.sgu.www.security.exception.GlobalException;
import cn.edu.sgu.www.security.restful.JsonResult;
import cn.edu.sgu.www.security.restful.ResponseCode;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletResponse;
import java.util.Objects;
/**
* 全局异常处理类
* @author heyunlin
* @version 1.0
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理GlobalException
* @param e GlobalException
* @return JsonResult<Void>
*/
@ExceptionHandler(GlobalException.class)
public JsonResult<Void> handleGlobalException(HttpServletResponse response, GlobalException e) {
printMessage(e);
response.setStatus(e.getResponseCode().getValue());
return JsonResult.error(e.getResponseCode(), e);
}
/**
* 处理BindException
* @param e BindException
* @return JsonResult<Void>
*/
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public JsonResult<Void> handleBindException(BindException e) {
printMessage(e);
BindingResult bindingResult = e.getBindingResult();
FieldError fieldError = bindingResult.getFieldError();
String defaultMessage = Objects.requireNonNull(fieldError).getDefaultMessage();
return JsonResult.error(ResponseCode.BAD_REQUEST, defaultMessage);
}
/**
* 处理Exception
* @param e Exception
* @return JsonResult<Void>
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public JsonResult<Void> handleException(Exception e) {
printMessage(e);
return JsonResult.error(ResponseCode.ERROR, e);
}
/**
* 打印异常信息
* @param e Exception
*/
private void printMessage(Exception e) {
System.out.println(e.getMessage());
e.printStackTrace();
}
}
业务类
User.java
package cn.edu.sgu.www.security.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* @author heyunlin
* @version 1.0
*/
@Data
@TableName("user")
public class User implements Serializable {
private static final long serialVersionUID = 18L;
@TableId(value = "id", type = IdType.INPUT)
private String id;
/**
* 姓名
*/
private String name;
/**
* 性别
*/
private Integer gender;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 手机号
*/
private String phone;
/**
* 是否启用
*/
private boolean enable;
/**
* 最后一次登录时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime lastLoginTime;
}
UserMapper.java
package cn.edu.sgu.www.security.mapper;
import cn.edu.sgu.www.security.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
/**
* @author heyunlin
* @version 1.0
*/
@Repository
public interface UserMapper extends BaseMapper<User> {
}
UserLoginDTO.java
package cn.edu.sgu.www.security.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
/**
* @author heyunlin
* @version 1.0
*/
@Data
public class UserLoginDTO implements Serializable {
private static final long serialVersionUID = 18L;
/**
* 用户名
*/
@NotBlank(message = "用户名不允许为空")
private String username;
/**
* 密码
*/
@NotBlank(message = "密码不允许为空")
private String password;
}
UserService.java
package cn.edu.sgu.www.security.service;
import cn.edu.sgu.www.security.dto.UserLoginDTO;
import cn.edu.sgu.www.security.entity.User;
/**
* @author heyunlin
* @version 1.0
*/
public interface UserService {
/**
* 登录认证
* @param userLoginDTO 用户登录信息
*/
void login(UserLoginDTO userLoginDTO);
/**
* 退出登录
*/
void logout();
/**
* 通过ID查询用户信息
* @param userId 用户ID
* @return User 通过ID查询到的用户信息
*/
User selectById(String userId);
}
UserController.java
package cn.edu.sgu.www.security.controller;
import cn.edu.sgu.www.security.dto.UserLoginDTO;
import cn.edu.sgu.www.security.entity.User;
import cn.edu.sgu.www.security.restful.JsonResult;
import cn.edu.sgu.www.security.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author heyunlin
* @version 1.0
*/
@RestController
@RequestMapping(path = "/user", produces = "application/json;charset=utf-8")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@RequestMapping(value = "/login", method = RequestMethod.POST)
public JsonResult<Void> login(@Validated UserLoginDTO userLoginDTO) {
userService.login(userLoginDTO);
return JsonResult.success("登录成功");
}
@RequestMapping(value = "/logout", method = RequestMethod.POST)
public JsonResult<Void> logout() {
userService.logout();
return JsonResult.success("登出成功");
}
@RequestMapping(value = "/selectById", method = RequestMethod.GET)
public JsonResult<User> selectById(@RequestParam(value = "id") String userId) {
User user = userService.selectById(userId);
return JsonResult.success(null, user);
}
}
UserServiceImpl.java
package cn.edu.sgu.www.security.service.impl;
import cn.edu.sgu.www.security.dto.UserLoginDTO;
import cn.edu.sgu.www.security.entity.User;
import cn.edu.sgu.www.security.mapper.UserMapper;
import cn.edu.sgu.www.security.service.UserService;
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.stereotype.Service;
/**
* @author heyunlin
* @version 1.0
*/
@Service
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
private final AuthenticationManager authenticationManager;
@Autowired
public UserServiceImpl(UserMapper userMapper, AuthenticationManager authenticationManager) {
this.userMapper = userMapper;
this.authenticationManager = authenticationManager;
}
@Override
public void login(UserLoginDTO userLoginDTO) {
Authentication authentication = new UsernamePasswordAuthenticationToken(
userLoginDTO.getUsername(),
userLoginDTO.getPassword()
);
authenticationManager.authenticate(authentication);
}
@Override
public void logout() {
// todo
}
@Override
public User selectById(String userId) {
return userMapper.selectById(userId);
}
}
配置类
SystemProperties.java
创建配置读取类SystemProperties
package cn.edu.sgu.www.security.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
* @author heyunlin
* @version 1.0
*/
@Data
@Component
@ConfigurationProperties(prefix = "system")
public class SystemProperties {
/**
* 登录页面
*/
private String loginPage;
/**
* 登录的请求地址
*/
private String loginUrl;
/**
* 登录成功后跳转的页面
*/
private String indexPage;
/**
* 退出登录的请求地址
*/
private String logoutUrl;
/**
* 白名单
*/
private List<String> whiteUrl;
/**
* 登录的参数
*/
private Map<String, String> parameter;
}
MybatisPlusConfig.java
package cn.edu.sgu.www.security.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Mybatis-Plus配置类
* @author heyunlin
* @version 1.0
*/
@Configuration
@MapperScan(basePackages = "cn.edu.sgu.www.security.mapper")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 防全表更新与删除插件
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
Spring Security相关的类
UserDetailsServiceImpl.java
UserDetailsService接口是Spring Security中非常重要的接口,在登录认证的时候会通过这个接口的loadUserByUsername()方法获取用户的信息,来完成登录的用户名、密码校验,完成登录流程。
package com.example.security.security;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.security.entity.User;
import com.example.security.exception.GlobalException;
import com.example.security.mapper.UserMapper;
import com.example.security.restful.ResponseCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
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.Component;
import java.util.ArrayList;
import java.util.List;
/**
* @author heyunlin
* @version 1.0
*/
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserMapper userMapper;
@Autowired
public UserDetailsServiceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户信息
User user = selectByUsername(username);
if (user == null) {
throw new BadCredentialsException("登录失败,用户名不存在!");
} else {
List<String> permissions = selectPermissions(username);
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.accountExpired(false)
.accountLocked(false)
.disabled(!user.getEnable())
.credentialsExpired(false)
.authorities(permissions.toArray(new String[] {}))
.build();
}
}
/**
* 通过用户名查询用户信息
* @param username 用户名
* @return User
*/
private User selectByUsername(String username) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username", username);
List<User> list = userMapper.selectList(wrapper);
if (list.size() == 1) {
return list.get(0);
}
return null;
}
/**
* 通过用户名查询用户权限
* @param username 用户名
* @return List<String>
*/
private List<String> selectPermissions(String username) {
if (username == null) {
throw new GlobalException(ResponseCode.BAD_REQUEST, "用户名不能为空");
}
List<String> permissions = new ArrayList<>();
permissions.add("/user/login");
permissions.add("/user/logout");
permissions.add("/user/selectById");
return permissions;
}
}
LoginSuccessHandler.java
登录成功的处理器
package cn.edu.sgu.www.security.security;
import cn.edu.sgu.www.security.config.SystemProperties;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author heyunlin
* @version 1.0
*/
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
private final SystemProperties systemProperties;
public LoginSuccessHandler(SystemProperties systemProperties) {
this.systemProperties = systemProperties;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
response.sendRedirect(systemProperties.getIndexPage());
}
}
LoginFailHandler.java
登录失败的处理器
package cn.edu.sgu.www.security.security;
import cn.edu.sgu.www.security.exception.GlobalException;
import cn.edu.sgu.www.security.restful.ResponseCode;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author heyunlin
* @version 1.0
*/
public class LoginFailHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
throw new GlobalException(ResponseCode.BAD_REQUEST, "登录失败,用户名或密码错误~");
}
}
SecurityConfig.java
在项目根包下创建security的配置类config.SecurityConfig
package cn.edu.sgu.www.security.config;
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.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author heyunlin
* @version 1.0
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final SystemProperties systemProperties;
@Autowired
public SecurityConfig(SystemProperties systemProperties) {
this.systemProperties = systemProperties;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return (String) charSequence;
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return charSequence.equals(s);
}
};
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 禁用防跨域攻击
http.csrf().disable();
// 配置各请求路径的认证与授权
http.formLogin()
.loginPage(systemProperties.getLoginPage()) // 自定义登录页面的地址
.loginProcessingUrl(systemProperties.getLoginUrl()) // 处理登录的接口地址
.usernameParameter(systemProperties.getParameter().get("username")) // 用户名的参数名
.passwordParameter(systemProperties.getParameter().get("password")) // 密码的参数名
.successHandler(new LoginSuccessHandler(systemProperties))
//.successForwardUrl("/index.html") // 登录成功跳转的地址
.failureHandler(new LoginFailHandler()); // 登录失败的处理器
// 退出登录相关配置
http.logout()
.logoutUrl(systemProperties.getLogoutUrl()) // 退出登录的接口地址
.logoutSuccessUrl(systemProperties.getLoginUrl()); // 退出登录成功跳转的地址
// 配置认证规则
String[] toArray = systemProperties.getWhiteUrl().toArray(new String[]{});
http.authorizeRequests()
.antMatchers(toArray).permitAll() // 白名单,也就是不需要登录也能访问的资源
.anyRequest().authenticated();
}
}
项目的包结构
文章就分享到这里了,文章的代码已上传到Gitee,可按需获取~
Spring Boot整合Spring Security案例项目https://gitee.com/muyu-chengfeng/springboot-springsecurity.git