安装依赖
spring-boot-starter-security
jjwt-api
jjwt-impl
jjwt-jackson
SpringSecurity
用数据库中的账号登录
Spring Security
的用户对象是 UserDetail类型,Spring Security
在认证流程中只认 UserDetail用户。具体而言:
spring security
提供了一个接口 UserDetailsService 来让用户提供账号和密码。用户实现这个接口中的loadUserByUsername方法,通过数据库中查询的账号和密码构造一个UserDetails对象返回给spring security
,然后框架自己完成认证操作。
具体做法:
1.创建UserDetailsServiceImpl类
在service/impl/user
包下创建UserDetailsServiceImpl类
实现UserDetailsService接口
中的loadUserByUsername方法
。
2.创建UserDetailsUtil类
在utils
包中创建UserDetailsUtil
实现 UserDetails接口
中的方法。首先定义一个User
,返回username
和passsword
//UserDetailsServiceImpl
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username",username);
User user = userMapper.selectOne(queryWrapper);
if (user == null) {
throw new UsernameNotFoundException(username);
}
return new UserDetailsUtil(user);
}
}
// UserDetailsUtil
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserDetailsUtil implements UserDetails {
private User user;
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of();
}
}
3.密码加密
若没配置加密,则数据库中的密码前面要加{noop},如{noop}123,则Security就知道该密码没有加密。
加密:在config
文件夹下配置SecurityConfig
类,使用encode
加密,matches
(明文,密文)进行匹配。
//SecurityConfig
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
JSON Web Tokens(JWT)
1.JwtUtil工具类
/**
* 为jwt工具类,用来创建、解析jwt token
* */
@Component
public class JwtUtil {
public static final long JWT_TTL = 60 * 60 * 1000L * 24 * 14; // 有效期14天
public static final String JWT_KEY = "JSDFSDFSDFASJDHASDASDDFCDFdJHASFDA67765asda123";
public static String getUUID() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, getUUID());
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
long expMillis = nowMillis + JwtUtil.JWT_TTL;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid)
.setSubject(subject)
.setIssuer("sg")
.setIssuedAt(now)
.signWith(secretKey, signatureAlgorithm)
.setExpiration(expDate);
}
public static SecretKey generalKey() {
byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
return new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256");
}
public static Claims parseJWT(String jwt) {
SecretKey secretKey = generalKey();
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(jwt)
.getBody();
}
}
2.JwtAuthenticationTokenFilter
/**
* 用来验证jwt token,如果验证成功,则将User信息注入上下文中
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserMapper userMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
token = token.substring(7);
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
throw new RuntimeException(e);
}
User user = userMapper.selectById(Integer.parseInt(userid));
if (user == null) {
throw new RuntimeException("用户名未登录");
}
UserDetailsUtil loginUser = new UserDetailsUtil(user);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
3.SecurityConfig类
.requestMatchers("/user/token/", "/user/register/").permitAll()
对上述两个路径放行,前端传数据,不需要加header。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
public SecurityConfig(JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter) {
this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter;
}
private static AuthorizationManager<RequestAuthorizationContext> hasIpAddress() {
IpAddressMatcher ipAddressMatcher = new IpAddressMatcher("127.0.0.1");
return (authentication, context) -> {
HttpServletRequest request = context.getRequest();
return new AuthorizationDecision(ipAddressMatcher.matches(request));
};
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/user/token/", "/user/register/").permitAll()
// .requestMatchers("/pk/start/game", "/pk/receive/bot/move").access(hasIpAddress())
.requestMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated()
);
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web -> web.ignoring().requestMatchers("/websocket/**"));
}
}
4.编写API
1.1.注入AuthenticationManager
@Autowired
private AuthenticationManager authenticationManager;
2.创建认证令牌
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
这行代码创建了一个 UsernamePasswordAuthenticationToken 对象,用于封装用户输入的用户名和密码。
3.进行认证
Authentication authenticate = authenticationManager.authenticate(token); //登录失败,会自动处理
这行代码使用 authenticationManager 对用户输入的令牌进行认证。认证管理器会根据配置的认证方式(例如数据库查询、LDAP 等)来验证用户的身份。如果认证失败,Spring Security 会自动处理异常(如抛出 BadCredentialsException)。
4.获取认证用户的详细信息:
UserDetailsUtil loginUser = (UserDetailsUtil) authenticate.getPrincipal();
User user = loginUser.getUser();
这两行代码从认证结果中获取 UserDetailsUtil 对象,这是一个包含用户详细信息的自定义对象。然后,从 UserDetailsUtil 对象中获取 User 对象,这个对象通常包含用户的基本信息(如用户名、角色等)。
5.生成jwtToken
String jwt = JwtUtil.createJWT(user.getId().toString());
@Service
public class UserServiceImpl implements UserService {
@Autowired
private AuthenticationManager authenticationManager;
@Override
public Map<String, String> getToken(String username, String password) {
//创建认证令牌
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
//进行认证
Authentication authenticate = authenticationManager.authenticate(token); //登录失败,会自动处理
//获取认证用户的详细信息
UserDetailsUtil loginUser = (UserDetailsUtil) authenticate.getPrincipal();
//获取用户
User user = loginUser.getUser();
//生成jwtToken
String jwt = JwtUtil.createJWT(user.getId().toString());
Map<String, String> map = new HashMap<>();
map.put("token", jwt);
map.put("error_message","success");
return map;
}
}
6.根据token获取用户信息
1.SecurityContextHolder.getContext().getAuthentication()
2.authentication.getPrincipal();
public ResultVo getUserInfo() {
UsernamePasswordAuthenticationToken authentication =
(UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
UserDetailsUtil loginUser = (UserDetailsUtil) authentication.getPrincipal();
//获取用户
User user = loginUser.getUser();
Map<String,String> map = new HashMap<>();
map.put("id",user.getId().toString());
map.put("username",user.getUsername());
return new ResultVo(0,"success",map);
}
前端部分
前端页面授权
const router = createRouter({ ... })
//设置守卫,只放行登录和注册页面
router.beforeEach((to, from, next) => {
const userStore = useUserStore();
if (!userStore.user.is_login && to.name !== 'login' && to.name !== 'register') {
next({ name: 'login' });
} else {
next(); //正常访问
}
});
注意:const userStore = useUserStore();在router中使用时 要放在方法里面,在外部使用会出错
登录状态持久化
1. 修改 state
const user = reactive({
id: localStorage.getItem('userId') || '',
username: localStorage.getItem('username') || '',
is_login: localStorage.getItem('isLogin') === 'true'
});
2.修改更新状态函数
const updateUser = (newUser) =>{
user.id = newUser.id;
user.username = newUser.username;
user.is_login = newUser.is_login;
localStorage.setItem('userId', newUser.id);
localStorage.setItem('username', newUser.username);
localStorage.setItem('isLogin', newUser.is_login);
}
3.修改退出函数
```
const logout = () => {
user.id = “”;
user.username = “”;
user.is_login = false;
localStorage.removeItem('userId');
localStorage.removeItem('username');
localStorage.removeItem('isLogin');
}
```