Spring Security-6.2 and spring boot
实现描述:在spring boot中引入新版的Security。
1、引入security
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、配置security
主要包含处理用户登录逻辑的AuthenticationManager以及负责放行以及其他控制的SecurityFilterChain。
引入security后,此时不做任何修改,启动项目,无论输入哪一个url都会发现进入security提供的登录页面。
此时就代表引入成功,其他的页面被security默认视为是私有资源,需要获得登录认证后授权才能访问。而登录界面由security生成,此时的账号密码可以在application.yml文件中配置。
security:
user:
name: test
password: 123456
用户登录
对于security而言,登录所需的用户由UserDetails这个接口实现,同时这个接口又实现了序列化,因此对于自己项目中的实体类来说,只需要实现这个接口就可以了。
public class User implements UserDetails
/**
* 获取用户权限
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {return null;}
@Override
public String getPassword() {return this.password;}
@Override
public String getUsername() {return this.username;}
/**
* 判断用户是否过期,未过期返回true
*/
@Override
public boolean isAccountNonExpired() {return true;}
/**
* 判断账号是否被锁定
*/
@Override
public boolean isAccountNonLocked() {return true;}
/**
* 判断用户凭据是否过期需要更新
*/
@Override
public boolean isCredentialsNonExpired() {return true;}
/**
* 判断用户是否可用
*/
@Override
public boolean isEnabled() {return true;}
以上是需要实现的方法。
在Mapper中,引入Mybatis-plus
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
此处继承BaseMapper类,不必再去创建xml。
关于登录逻辑,security交由UserDetailsService实现用户检验,因此只需要实现这个接口即可。
@Service
public class UserDetailServiceImpl implements UserDetailsService
需要实现的方法只有一个。
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, username));
log.info("loadUserByUsername:" + user);
return user;
}
此时,只是重写了security对于登录逻辑的接口,我们还需要自定义登录的内容,例如抛出用户名与密码错误的异常等。
定义登录接口
public interface UserService extends IService<User> {
int register(User user);
String login(User user);
}
实现登录接口
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Resource
AuthenticationManager authenticationManager;
@Resource
UserMapper userMapper;
@Resource
PasswordEncoder passwordEncoder;
@Resource
JwtUtil jwtUtil;
@Override
public int register(User user) {
String password = passwordEncoder.encode(user.getPassword());
user.setPassword(password);
userMapper.insert(user);
return 0;
}
@Override
public String login(User user) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
Authentication authentication = null;
try {
authentication = authenticationManager.authenticate(authenticationToken);
} catch (AuthenticationException e) {
log.info("用户名或密码错误!");
return "用户名或密码错误!";
}
User user1 = (User) authentication.getPrincipal();
Map<String, Object> map = new HashMap<>();
map.put("username", user1.getUsername());
log.info("username{}", user1.getUsername());
String token = jwtUtil.createToken(map);
System.out.println(token);
return token;
}
}
此处引入了认证管理器与密码编码和自定义的JWT工具类。
JWT令牌
@Component
public class JwtUtil {
private String serectKey = "feqguifebahjfhabdsbh";
public String createToken(Map<String, Object> map) {
return Jwts.builder()
.setClaims(map)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 1000))
.signWith(SignatureAlgorithm.HS256, serectKey)
.compact();
}
public Claims paresToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(serectKey)
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
claims = e.getClaims();
}
return claims;
}
}
定义一个生成token的方法和解析的方法。注意秘钥与有效期。
@Controller
public class AutoController {
@Resource
UserService userService;
@PostMapping("/dologin")
@ResponseBody
public String login(User user) {
String token = userService.login(user);
return token;
}
}
在控制层,抛出登录接口。
前端页面简单使用表单即可。
<form action="/dologin" method="post">
账号<input type="text" name="username">
密码<input type="password" name="password">
<input type="submit" value="login">
</form>
此时我们只是定义了自己的登录逻辑,但还没有对security进行配置。
@Configuration
@EnableWebSecurity
public class SecurityConfig
我们需要使用两个注解,缺一不可。
在我们自己的业务类中,我们引入了密码编码,因此在此处需要定义。
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
使用@Bean方法注解写入即可。
认证管理器也需要定义,其中需要把自定义的UserDetailsService和密码管理器插入。
@Bean
public AuthenticationManager authenticationManager(PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return new ProviderManager(provider);
}
此时来配置过滤器链。
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable());
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/register", "/")
.permitAll()
.anyRequest()
.authenticated()
);
http.formLogin(
form -> form.loginPage("/login")
.loginProcessingUrl("/dologin")
.usernameParameter("username")
.passwordParameter("password")
.failureForwardUrl("/demo")
// .successForwardUrl("/")
);
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
由于我们使用自定义的界面,所以需要禁用csrf,这个在原始的登录页面中被隐藏的用于防劫持的input标签。之后,我们需要开放所有用户和请求都可以访问的地址,其中包括登录注册和首页,除此之外的任何请求都需要验证。
之后我们需要处理登录表单,指定登录页面,和登录接口,以及参数和失败后跳转的页面,此处讲成功后跳转的页面注释,是因为会与自己的业务产生冲突。
最后需要进行JWT验证,因此在账号密码过滤器前添加了自定义的JWT的过滤器。
返回构建。
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Resource
JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (token == null) {
doFilter(request, response, filterChain);
return;
}
Claims claims = null;
claims = jwtUtil.paresToken(token);
System.out.println("claims: " + claims);
String username = claims.get("username", String.class);
User user = new User();
user.setUsername(username);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
System.out.println("authenticationToken: " + authenticationToken);
doFilter(request, response, filterChain);
}
}
Claims是Java中专用于对JWT声明的类,包含了JWT中声明的所有的键值对。
授权
权限模型
RBAC,基于角色的访问控制。
数据库表设计
-- 角色表
CREATE TABLE Roles (
RoleID INT PRIMARY KEY,
RoleName VARCHAR(50) UNIQUE
);
-- 权限表
CREATE TABLE Permissions (
PermissionID INT PRIMARY KEY,
PermissionName VARCHAR(100) UNIQUE
);
-- 用户表
CREATE TABLE Users (
UserID INT PRIMARY KEY,
UserName VARCHAR(50) UNIQUE,
Password VARCHAR(100), -- 注意:密码最好加密存储
Email VARCHAR(100) UNIQUE
);
-- 用户角色关联表
CREATE TABLE UserRoles (
UserRoleID INT PRIMARY KEY,
UserID INT,
RoleID INT,
UNIQUE(UserID, RoleID)
);
-- 角色权限关联表
CREATE TABLE RolePermissions (
RolePermissionID INT PRIMARY KEY,
RoleID INT,
PermissionID INT,
UNIQUE(RoleID, PermissionID)
);
OAuth2.0(第三方授权登录,颁发令牌)