简介
Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。
一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。
一般Web应用的需要进行认证和授权。
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作
而认证和授权也是SpringSecurity作为安全框架的核心功能。
准备工作
需要准备一个Springboot工程,完整的demo项目在这,欢迎指教。
下面给出项目的依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
数据库文件可以在若依官网获取,本文的若依版本是3.8.7。不同版本数据库文件应该变化不大。
主要使用的是sys_user_role、sys_user、sys_role这三个表
创建一个test文件,验证mp和数据库配置是否正确。
Mapper 和 对应 Service 这里不做说明。
认证
Controller如下。业务写在了Controller内部,简化代码。
package com.ss.controller;
import com.ss.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.Objects;
@RestController
public class LoginController {
@Autowired
private AuthenticationManager authenticationManager;
@PostMapping("/login")
// @PreAuthorize("@ss.hasRole('admin', #user)") // 鉴权用,认证可以先注释掉
@ResponseBody
public String login(@RequestBody User user) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
if(Objects.isNull(authenticate)){
throw new RuntimeException("用户名或密码错误");
}
return "111";
}
}
User实体类
package com.ss.domain;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* 数据库中的 user
*/
@Data
@TableName("sys_user")
public class User {
/**
* 用户名
*/
private String userName;
/**
* 用户密码
*/
private String password;
/**
* 用户ID
*/
private Long userId;
}
实现UserDetailsService接口的 loadUserByUsername通过用户名加载User对象方法。
package com.ss.security;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ss.domain.User;
import com.ss.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
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.Service;
import java.util.Objects;
@Service
public class LoginUserImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<User> eq = new LambdaQueryWrapper<User>().eq(User::getUserName, username);
User dbUser = userService.getOne(eq);
//如果查询不到数据就通过抛出异常来给出提示
if(Objects.isNull(dbUser)){
throw new RuntimeException("用户名或密码错误");
}
return createLoginUser(dbUser);
}
private UserDetails createLoginUser(User dbUser) {
return new SecurityUser(dbUser);
}
}
因为 loadUserByUsername的返回值类型是 UserDetails 类型的,所以需要将 User类型转换成 UserDetails类型。
这里使用 SecurityUser类完成这个功能,使用它实现UserDetails 接口
package com.ss.security;
import com.ss.domain.User;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
@Data
@NoArgsConstructor
public class SecurityUser implements UserDetails {
/**
* 用户名 和 密码
*/
private User user;
public SecurityUser(User dbUser) {
this.user = dbUser;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
最后是ss的配置类
package com.ss.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
// @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SsConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//关闭csrf
http.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
//把token校验过滤器添加到过滤器链中
// http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
授权
打开Controller的授权注解
添加实体类Role和UserRole
自定义授权类,这里使用了hasRole判断登录用户是否具有某个角色。
package com.ss.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ss.domain.Role;
import com.ss.domain.User;
import com.ss.domain.UserRole;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Objects;
@Service("ss")
public class PermissionService {
@Autowired
private RoleService roleService;
@Autowired
private UserRoleService userRoleService;
/**
* 判断用户是否拥有某个角色
*
* @param role 角色字符串
* @return 用户是否具备某角色
*/
public boolean hasRole(String role, User loginUser)
{
LambdaQueryWrapper<UserRole> eq = new LambdaQueryWrapper<UserRole>().eq(UserRole::getUserId, loginUser.getUserId());
UserRole one = userRoleService.getOne(eq);
String roleId = one.getRoleId();
if (roleId.isEmpty()) {
return false;
}
LambdaQueryWrapper<Role> eq1 = new LambdaQueryWrapper<Role>().eq(Role::getRoleId, roleId);
Role one1 = roleService.getOne(eq1);
String roleKey = one1.getRoleKey();
if (!Objects.isNull(roleKey) && roleKey.equals(role.trim())) {
return true;
}
return false;
}
}
测试
使用 HTTPClient进行测试
POST http://localhost:7878/login
Content-Type: application/json
{
"userName": "ry",
"password": "admin123",
"userId": 2
}
POST http://localhost:7878/login
Content-Type: application/json
{
"userName": "admin",
"password": "admin123",
"userId": 1
}