学习链接:https://www.bilibili.com/video/BV1mm4y1X7Hc/?p=7&spm_id_from=pageDriver&vd_source=4cb41c702c93d4d052ec8d19f316525f
一、 搭建springboot项目
1. 创建maven项目
2. 导入基本依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
3. 引入SpringSecurity
3.1 引入SpringSecurity依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
引入依赖后就需要登录后才能访问接口,用户名是user,密码在控制台输出
4. 认证
4.1 登录校验流程
4.2 springSecurity完整流程
4.3 认证流程详解
二、自定义登录
1. 思路分析
登录
① 自定义登录接口
调用providerManager的方法进行认证,认证通过就生成jwt并把用户信息存入redis
② 自定义UserDetail接口实现类
在数据库查询用户信息
鉴权
①定义jwt认证过滤器
获取token,解析token获取userid,根据userid在redis查询用户信息,存入SecurityContextHolder
2. 准备工作
2.1 导入相关依赖
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--fastjson依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<!-- jwt依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
2.2 redis配置类
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings(value = {"unchecked", "rawTypes"})
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
FastjsonRedisSerializer serializer = new FastjsonRedisSerializer(Object.class);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
2.3 序列化工具类
public class FastjsonRedisSerializer<T> implements RedisSerializer<T> {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
static {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}
public FastjsonRedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException {
if (t == null) {
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (bytes == null || bytes.length <= 0) {
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz);
}
protected JavaType getJavaType(Class<?> clazz) {
return TypeFactory.defaultInstance().constructType(clazz);
}
}
2.4 jwt工具类
public class JwtUtils {
/**
* 两个常量: 过期时间;秘钥
*/
public static final long EXPIRE = 1000*60*60*24;
public static final String SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";
/**
* 生成token字符串的方法
* @param id
* @return
*/
public static String getJwtToken(String id){
String JwtToken = Jwts.builder()
//JWT头信息
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS2256")
//设置分类;设置过期时间 一个当前时间,一个加上设置的过期时间常量
.setSubject("lin-user")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
//设置token主体信息,存储用户信息
.claim("id", id)
//.signWith(SignatureAlgorithm.ES256, SECRET)
.signWith(SignatureAlgorithm.HS256, SECRET)
.compact();
return JwtToken;
}
/**
* 判断token是否存在与有效
* @Param jwtToken
*/
public static boolean checkToken(String jwtToken){
if (StringUtils.isEmpty(jwtToken)){
return false;
}
try{
//验证token
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(jwtToken);
}catch (Exception e){
e.printStackTrace();
return false;
}
return true;
}
/**
* 判断token是否存在与有效
* @Param request
*/
public static boolean checkToken(HttpServletRequest request){
try {
String token = request.getHeader("token");
if (StringUtils.isEmpty(token)){
return false;
}
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
}catch (Exception e){
e.printStackTrace();
return false;
}
return true;
}
/**
* 根据token获取会员id
* @Param token
*/
public static String getMemberIdByJwtToken(String token){
if (StringUtils.isEmpty(token)){
return "";
}
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
Claims body = claimsJws.getBody();
return (String) body.get("id");
}
}
2.5 实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = -1L;
private Long id;
private String userName;
private String nickName;
private String password;
// 0:正常 1:停用
private int status;
private String email;
private String phonenumber;
// 0:男 1:女 2:未知
private int sex;
public User(Long id, String userName, String password) {
this.id = id;
this.userName = userName;
this.password = password;
}
}
3.实现
3.1 环境准备
3.1.1 建表
3.1.2 引入数据库相关依赖
3.1.3 添加数据库配置
spring:
datasource:
url:
username:
password:
driver-class-name:
3.1.4 数据层
3.2 创建UserDetailService接口的实现类
实现UserDetailService接口,重写loadUserByUsername方法
该方法需要返回UserDetails类型,创建一个UserDetails接口的实现类,并重写相关方法
@Data
public class LoginUser implements UserDetails {
// 用户信息
private User user;
// 权限列表
private List<String> permissions;
public LoginUser(User user, List<String> permissions) {
this.user = user;
this.permissions = permissions;
}
public LoginUser(User user) {
this.user = user;
}
@JSONField(serialize = false)
private List<GrantedAuthority> authorities;
// 获取用户权限信息
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (authorities == null) {
authorities = permissions.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
return authorities;
}
// 获取用户账户
@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;
}
}
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//todo 这里应该查询数据库,简单测试直接返回一个固定对象
// {noop} 表示密码是明文
return new LoginUser(new User(1L, "zs", "{noop}1234"), Arrays.asList("test"));
}
}
3.3 密码加密
① 默认使用的passwordEncoder需要数据库中密码的格式为{id}password,会根据id判断加密方式,一般不采用这种方式。所以就需要替换PasswordEncoder
② 一般使用SpringSecurity提供的BCryptPasswordEncoder,直接注入使用
相同密码每次加密后都不一样,使用matches方法比较
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
3.4 自定义登录接口
3.4.1 controller
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
@GetMapping("/user/login")
public String login(@RequestParam("userName") String userName,
@RequestParam("password") String password) {
return loginService.login(userName, password);
}
}
3.4.2 service
在SecurityConfig中添加代码
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
注入上面的bean,进行认证
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Override
public String login(String userName, String password) {
// authenticationManager.authenticate进行用户认证
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userName, password);
Authentication authenticate = authenticationManager.authenticate(token);
// 认证没通过,给出对应提示
if (Objects.isNull(authenticate)) {
throw new RuntimeException("登录失败");
}
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
//todo 将用户信息存入redis
String jwtToken = JwtUtils.getJwtToken(loginUser.getUser().getId().toString());
System.out.println(JwtUtils.getMemberIdByJwtToken(jwtToken));
// 登陆成功,返回jwt
return jwtToken;
}
}
3.4.3 放行登录接口
SecurityConfig类中重写configure方法
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 关闭csrf
.csrf().disable()
// 不通过session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 放行登录接口
.antMatchers("/user/login").anonymous()
// 其他所有请求都需要登录鉴权
.anyRequest().authenticated();
}
3.5 认证
3.5.1 创建认证过滤器
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = httpServletRequest.getHeader("token");
if (!StringUtils.hasText(token)) {
// 放行,后面过滤器会抛出异常的
filterChain.doFilter(httpServletRequest, httpServletResponse);
// 返回,避免
return;
}
// 解析token
String id = JwtUtils.getMemberIdByJwtToken(token);
//todo 根据id从redis取
LoginUser loginUser = new LoginUser(new User(Long.parseLong(id), "zs", "{noop}1234"), Arrays.asList("test"));
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
// 存入SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
3.5.2 将认证过滤器添加到UsernamePasswordAuthenticationFilter之前
① 注入过滤器
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
② 在SecurityConfig的configure方法中添加代码
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
三、退出登录
实现
删除redis中的用户信息就行
@Override
public void logout() {
// 获取认证信息
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long id = loginUser.getUser().getId();
System.out.println(id);
//todo 根据id删除redis用户信息
}
四、 鉴权
1. 设置访问权限
1.1 SecurityConfig类上添加@EnableGlobalMethodSecurity注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
1.2 在需要鉴权的接口上添加@PreAuthorize注解
// 表示访问此接口需要read权限
@PreAuthorize("hasAuthority('read')")
@GetMapping("/hello")
public String hello() {
return "hello";
}
// 表示访问此接口需要角色是system::admin
@PreAuthorize("hasRole('system::admin')")
@GetMapping("/hello1")
public String hello1() {
return "hello1";
}
2. 封装权限信息
2.1 查询权限信息
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 第二个参数是权限列表,根据用户名将查询出的权限放入
return new LoginUser(new User(1L, "zs", "{noop}1234"), Arrays.asList("test"));
}
}
2.2 UserDetails是使用getAuthorities方法获取权限,所以LoginUser需要重写getAuthorities方法
// 定义成成员变量,避免每次调用方法都转换
// 只有authorities为空时才转换
// 注解作用:不序列化当前变量
@JSONField(serialize = false)
private List<GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 将permission中的String类型的权限转换为GrantedAuthority类型
if (authorities == null) {
authorities = permissions.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
return authorities;
}
3. 校验权限信息
在JwtAuthenticationTokenFilter 的doFilterInternal方法中将用户的权限信息封装到UsernamePasswordAuthenticationToken中
// 在redis中查询用户信息loginUser
// 第三个参数loginUser.getAuthorities()就是用户的权限信息
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());