一、快速开始
1.创建工程
创建一个名为SpringSecurity5.7的工程文件,SpringBoot的版本选择2.7
2.引入依赖
<!-- 引入Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 引入SpringSecurity -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
启动程序,在你的浏览器中输入localhost:端口号,这是登录用户名默认是user,密码会打印在控制台上
二、搭建SpringSecurity认证
1.引入Jwt依赖进行token验证
<!-- Token生成与解析-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2.创建JWT工具类
public class JwtUtils {
private static String subject = "jwt";
private static String secretKey = "spell a";
public static final Long ExpireTime = 1000L * 60 * 60 * 24;
public static JwtBuilder getJwtBuilder(String uuid, Long expireTime){
Long expireMillis = System.currentTimeMillis() + expireTime;
return Jwts.builder()
.setId(uuid)
.setSubject(subject)
.setIssuer("jjwt")
.setIssuedAt(new Date(System.nanoTime()))
.signWith(SignatureAlgorithm.HS256,secretKey)
.setExpiration(new Date(expireMillis));
}
public static String getUUID(){
return UUID.randomUUID().toString().replace("-","");
}
public static String createToken(String uuid){
return getJwtBuilder(uuid,ExpireTime).compact();
}
public static Claims parseToken(String token){
try {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
}catch (Exception e){
return null;
}
}
public static Boolean isExpiration(Claims claims){
return claims.getExpiration().before(new Date());
}
public static void main(String[] args) {
String token = createToken(getUUID());
System.out.println(token);
Claims claims = parseToken(token);
System.out.println(claims);
}
}
3.引入mybatis-plus依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
4.创建用户实体类
public class SysUser extends BasePojo implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long userId;
private String userName;
private String password;
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
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;
}
}
5.创建SysUserMapper
@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {
}
6.yml文件 连接配置
spring:
application:
name: SpringSecurity5.7
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://localhost:3306/mydatabase?useSSL=false&characterEncoding=utf-8&serverTimezone=GMT%2B8
7.创建响应码枚举对象
public enum ResultCode {
SUCCESS(200,"操作成功!"),
ERROR(401,"操作失败!");
private Integer code;
private String msg;
ResultCode(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public ResultCode setCode(Integer code) {
this.code = code;
return this;
}
public String getMsg() {
return msg;
}
public ResultCode setMsg(String msg) {
this.msg = msg;
return this;
}
}
8.搭建全局异常处理
@RestControllerAdvice
public class SpaExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseResult handle(MethodArgumentNotValidException e){
List<FieldError> fieldError = e.getFieldErrors();
StringBuilder str = new StringBuilder();
fieldError.forEach(item-> str.append(item.getDefaultMessage()));
return ResponseResult.error(400,str.toString());
}
@ExceptionHandler(SpellaException.class)
public ResponseResult handle(SpellaException e){
e.printStackTrace();
return ResponseResult.error(e.getMessage());
}
@ExceptionHandler(RuntimeException.class)
public ResponseResult handle(RuntimeException e){
e.printStackTrace();
return ResponseResult.error(e.getMessage());
}
}
9.自定义异常处理
public class SpellaException extends RuntimeException{
private Integer code;
private String msg;
public SpellaException(Integer code, String msg) {
super(msg);
this.code = code;
this.msg = msg;
}
public SpellaException(String msg) {
super(msg);
this.code = 400;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
10.创建LoginUser实现UserDetails接口
public class LoginUser implements UserDetails, Serializable {
private SysUser sysUser;
private List<String> permissions;
public LoginUser(SysUser sysUser, List<String> permissions) {
this.sysUser = sysUser;
this.permissions = permissions;
}
@JsonIgnore
private List<SimpleGrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
authorities = this.permissions.stream().map(item -> new SimpleGrantedAuthority(item)).collect(Collectors.toList());
return authorities;
}
@Override
@JsonIgnore
public String getPassword() {
return sysUser.getPassword();
}
@Override
@JsonIgnore
public String getUsername() {
return sysUser.getUserName();
}
@Override
@JsonIgnore
public boolean isAccountNonExpired() { //帐户是否没有过期
return true;
}
@Override
@JsonIgnore
public boolean isAccountNonLocked() { //帐户是否没有锁定
return true;
}
@Override
@JsonIgnore
public boolean isCredentialsNonExpired() { //凭据是否没有过期
return true;
}
@Override
@JsonIgnore
public boolean isEnabled() { //是否已启用
return true;
}
}
11.创建UserDetailServiceImpl实现UserDetailsService接口
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Resource
private SysUserMapper sysUserMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1. 根据用户名查询数据库
SysUser sysUser = sysUserMapper.selectOne(new QueryWrapper<SysUser>().eq("user_name", username));
if(ObjectUtil.isNull(sysUser)) {
throw new RuntimeException("用户名错误!");
}
//2. 查询权限信息
List<String> permissions = null;
//3. 返回UserDetails
return new LoginUser(sysUser,permissions );
}
}
12.创建SecurityConfig
@Configuration
@EnableWebSecurity //添加 security 过滤器
@EnableGlobalMethodSecurity(prePostEnabled = true) //启用方法级别的认证
public class SecurityConfig {
private static final String[] patters = {"/auth/login"};
@Resource
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Resource
private AuthenticationEntryPointImpl authenticationEntryPoint;
@Resource
private AccessDeniedHandlerImpl accessDeniedHandler;
//密码解析器
@Bean
public PasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
//关闭csrf
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() //不通过session获取SecurityContext
.authorizeRequests()
.antMatchers(patters).anonymous() //只允许未登录匿名者访问
.anyRequest().authenticated().and() //除上面所有请求都需要鉴权验证
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and() //认证失败返回信息
.exceptionHandling().accessDeniedHandler(accessDeniedHandler).and() //授权失败 没有权限
.cors().and()
.build();
}
@Autowired
private AuthenticationConfiguration authenticationConfiguration;
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
13.密码解析器
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.security.crypto.bcrypt;
import java.security.SecureRandom;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.crypto.password.PasswordEncoder;
public class BCryptPasswordEncoder implements PasswordEncoder {
private Pattern BCRYPT_PATTERN;
private final Log logger;
private final int strength;
private final BCryptPasswordEncoder.BCryptVersion version;
private final SecureRandom random;
public BCryptPasswordEncoder() {
this(-1);
}
public BCryptPasswordEncoder(int strength) {
this(strength, (SecureRandom)null);
}
public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version) {
this(version, (SecureRandom)null);
}
public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, SecureRandom random) {
this(version, -1, random);
}
public BCryptPasswordEncoder(int strength, SecureRandom random) {
this(BCryptPasswordEncoder.BCryptVersion.$2A, strength, random);
}
public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, int strength) {
this(version, strength, (SecureRandom)null);
}
public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, int strength, SecureRandom random) {
this.BCRYPT_PATTERN = Pattern.compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}");
this.logger = LogFactory.getLog(this.getClass());
if (strength == -1 || strength >= 4 && strength <= 31) {
this.version = version;
this.strength = strength == -1 ? 10 : strength;
this.random = random;
} else {
throw new IllegalArgumentException("Bad strength");
}
}
public String encode(CharSequence rawPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
} else {
String salt;
if (this.random != null) {
salt = BCrypt.gensalt(this.version.getVersion(), this.strength, this.random);
} else {
salt = BCrypt.gensalt(this.version.getVersion(), this.strength);
}
return BCrypt.hashpw(rawPassword.toString(), salt);
}
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
} else if (encodedPassword != null && encodedPassword.length() != 0) {
if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
this.logger.warn("Encoded password does not look like BCrypt");
return false;
} else {
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
} else {
this.logger.warn("Empty encoded password");
return false;
}
}
public boolean upgradeEncoding(String encodedPassword) {
if (encodedPassword != null && encodedPassword.length() != 0) {
Matcher matcher = this.BCRYPT_PATTERN.matcher(encodedPassword);
if (!matcher.matches()) {
throw new IllegalArgumentException("Encoded password does not look like BCrypt: " + encodedPassword);
} else {
int strength = Integer.parseInt(matcher.group(2));
return strength < this.strength;
}
} else {
this.logger.warn("Empty encoded password");
return false;
}
}
public static enum BCryptVersion {
$2A("$2a"),
$2Y("$2y"),
$2B("$2b");
private final String version;
private BCryptVersion(String version) {
this.version = version;
}
public String getVersion() {
return this.version;
}
}
}
14.创建登录认证过滤器 JwtAuthenticationFilter
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Resource
private RedisUtils redisUtils;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = request.getHeader(LoginConstants.AUTH);
if(StrUtil.isBlank(token)){
filterChain.doFilter(request,response);
return;
}
//判断token是否合法
if(!token.startsWith(LoginConstants.BEARER)) {
throw new SpellaException("token错误!");
}
token = token.substring(token.indexOf(" ")+1);
//解析token
Claims claims = JwtUtils.parseToken(token);
if(ObjectUtil.isNull(claims)) {
throw new SpellaException("token解析失败!");
}
if(JwtUtils.isExpiration(claims)) {
throw new SpellaException("token已过期!请重新登录!");
}
//封装Authentication
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
//存入SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request,response);
}
}
15.自定义用户权限认证失败响应 AccessDeniedHandlerImpl
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
String jsonString = JSONObject.toJSONString(ResponseResult.error(401, "您没有访问权限,请重新登录!"));
WebUtils.renderString(response,jsonString);
}
}
16.自定义用户登录认证失败响应 AuthenticationEntryPointImpl
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
String jsonString = JSONObject.toJSONString(ResponseResult.error(401, "系统认证失败,请重新登录!"));
WebUtils.renderString(response,jsonString);
}
}
三、搭建登录接口
1.创建登录映射对象
public class LoginParam {
@NotBlank(message = "用户名不能为空!")
private String userName;
@NotBlank(message = "密码不能为空!")
private String password;
private String token;
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
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;
}
}
2.创建数据返回对象
public class ResponseResult <T>{
private Integer code;
private String msg;
private T data;
public ResponseResult(){}
public ResponseResult(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public static <T> ResponseResult build(ResultCode resultCode,T data){
return new ResponseResult(resultCode.getCode(),resultCode.getMsg(),data);
}
public static <T> ResponseResult ok(){
return build(ResultCode.SUCCESS,null);
}
public static <T> ResponseResult ok(T data){
return build(ResultCode.SUCCESS.setMsg("操作成功!"),data);
}
public static <T> ResponseResult ok(String msg){
return build(ResultCode.SUCCESS.setMsg(msg),null);
}
public static <T> ResponseResult ok(String msg,T data){
return build(ResultCode.SUCCESS.setMsg(msg),data);
}
public static <T> ResponseResult error(String msg){
return build(ResultCode.ERROR.setMsg(msg).setCode(401),null);
}
public static <T> ResponseResult error(Integer code,String msg){
return build(ResultCode.ERROR.setCode(code).setMsg(msg),null);
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
3.创建AuthController
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private AuthService authService;
@PostMapping("/login")
public ResponseResult login(@Valid @RequestBody LoginParam param){
return authService.login(param);
}
}
4.创建AuthService
public interface AuthService {
ResponseResult login(LoginParam param);
}
5.创建AuthServiceImpl
@Service
public class AuthServiceImpl implements AuthService{
@Resource
private AuthenticationManager authenticationManager;
@Override
public ResponseResult login(LoginParam sysUser) {
//方法验证
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(sysUser.getUserName(), sysUser.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//校验失败
if(ObjectUtil.isNull(authenticate) || ObjectUtil.isNull(authenticate.getPrincipal())) {
throw new SpellaException("用户名或密码错误!");
}
LoginUser loginUser = (LoginUser)(authenticate.getPrincipal());
Long id = loginUser.getSysUser().getUserId();
String token = JwtUtils.createToken(id.toString());
HashMap<String, Object> map = new HashMap<>();
map.put("token",token);
return ResponseResult.ok("登录成功!",map);
}
}
6.登录测试
postman测试
四、总结
以上就是一个简单的SpringBoot整合SpringSecurity的案例