1、引入Spring Security依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2、添加配置类
import com.company.da.project.auth.JwtTokenFilter;
import com.company.da.project.auth.MyAuthenticationEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.time.Duration;
import java.util.Collections;
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtTokenFilter jwtTokenFilter;
@Autowired
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
/**
* 对密码进行加密的类,配置在spring中,方便调用
* 例如:bCryptPasswordEncoder.encode(registerUser.get("password"))
* 是 PasswordEncoder实现类
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncode() {
return new BCryptPasswordEncoder();
}
/**
* loginService中验证usernamePasswordAuthenticationToken中用户名和密码
*/
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
/**
* 重写configure方法,根据项目需求配置,并将将写好的相应配置类引入
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//关闭session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//授权
.authorizeRequests()
//对于登陆接口允许访问/login是不需要带项目路径/dac的
.antMatchers("/login").anonymous()
//其他接口要走验证
.anyRequest().authenticated();
http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
http.cors().configurationSource(corsConfigurationSource());//允许跨域
//增加授权异常处理
http.exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint);
}
/**
* 解决跨域问题
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowCredentials(true);
configuration.setAllowedOrigins(Collections.singletonList("*"));
configuration.setAllowedMethods(Collections.singletonList("*"));
configuration.setAllowedHeaders(Collections.singletonList("*"));
configuration.setMaxAge(Duration.ofHours(1));
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
过滤器,每次请求进行token验证
import com.company.da.project.common.RedisConstant;
import com.company.da.project.dto.JwtUser;
import com.company.da.project.dto.RedisUser;
import com.company.da.project.util.JsonUtils;
import com.company.da.project.util.RedisUtil;
import com.company.da.project.util.coding.Coding;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 这个拦截器将redis中的用户信息获取到,放到context中,后面的filter看见上下问中有用户信息,说明是验证过的,直接放行
*/
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisUtil redisUtil;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
//1、获取token
//2、解析token
//3、获取用户信息,可以是redis或者数据库
//4、将用户信息存入应用上下文中
String userId = request.getHeader("token");
String redisUserStr = (String) redisUtil.get(RedisConstant.ACS_TOKEN + userId);
if (Coding.notNull(redisUserStr)) {
RedisUser redisUser = JsonUtils.toBean(redisUserStr, RedisUser.class);
if (Coding.notNull(redisUser)) {
//将用户信息放进去
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(new JwtUser(redisUser), null, null);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//应用上下文中设置登录用户信息,此时Authentication类型为User
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
//如果token为空,直接放行,后面会有拦截器处理
chain.doFilter(request, response);
}
}
实现UserDetailsService接口,对用户查询进行支持
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.company.da.project.common.exception.PlatformException;
import com.company.da.project.dto.JwtUser;
import com.company.da.project.repository.entity.UserInfo;
import com.company.da.project.repository.mapper.UserInfoMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author: jasmine
*/
@Service
public class UserDetialServiceImpl implements UserDetailsService {
@Autowired
private UserInfoMapper userInfoMapper;
@Override
public UserDetails loadUserByUsername(String username) throws PlatformException {
//查询用户信息
List<UserInfo> userInfos = userInfoMapper.selectList(Wrappers.lambdaQuery(UserInfo.class).eq(UserInfo::getName, username));
UserInfo userInfo = userInfos.stream().findFirst().orElse(null);
if (null == userInfo) {
throw new PlatformException("用户名或者密码错误");
}
//TODO 查询对应的权限信息
//封装UserDetails对象
return new JwtUser(userInfo);
}
}
用户实体类
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.company.da.project.util.MyUtil;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
/**
* @author jasmine
*/
@Getter
@Setter
@NoArgsConstructor
@TableName(value = "da_user_info")
public class UserInfo {
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户唯一ID
*/
private String userId;
/**
* 用户名
*/
private String name;
/**
* 昵称
*/
private String nickName;
/**
* 手机号
*/
private String mobile;
/**
* 密码
*/
private String password;
/**
* 出生日期
*/
private String birthday;
/**
* 性别,0未知,1男,2女
*/
private Integer sex;
/**
* 用户来源
*/
private Integer sourceFrom;
/**
* 头像
*/
private String avatar;
/**
* 状态
*/
private Integer state;
/**
* 是否删除
*/
private Integer isdel;
/**
* 更新时间
*/
private LocalDateTime actionTime;
/**
* 创建时间
*/
private LocalDateTime createTime;
public UserInfo(String userName, String encodePwd) {
this.name = userName;
this.password = encodePwd;
this.userId = MyUtil.getUniqueId();
}
}
JWTUser对象类,对UserDetails接口实现,支持框架中用户信息传递
import com.company.da.project.repository.entity.UserInfo;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* @author: jasmine
*/
@Getter
@Setter
@NoArgsConstructor
public class JwtUser implements UserDetails {
/**
* 主键ID
*/
private Long id;
/**
* 用户唯一ID
*/
private String userId;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 权限信息
*/
private Collection<? extends GrantedAuthority> authorities;
/**
* 无参构造器
*/
public JwtUser(UserInfo userInfo) {
this.username = userInfo.getName();
this.userId = userInfo.getUserId();
this.password = userInfo.getPassword();
this.id = userInfo.getId();
}
public JwtUser(RedisUser redisUser) {
this.username = redisUser.getUserName();
this.userId = redisUser.getUserId();
this.password = redisUser.getPassword();
this.id = redisUser.getId();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
处理认证失败异常,当有认证失败的时候,封装请求体为项目中Response标准格式
import com.company.da.project.common.Response;
import com.company.da.project.util.JsonUtils;
import com.company.da.project.util.coding.Coding;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 未认证异常处理
*/
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
Response<String> toLogin = Response.error(Coding.strOrEmpty(HttpStatus.UNAUTHORIZED.value()), "未认证,请登陆");
String res = JsonUtils.toString(toLogin);
response.setStatus(401);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
try {
response.getWriter().write(res);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
业务接口Controller,定义登陆、退出、注册接口
import com.company.da.project.common.Response;
import com.company.da.project.controller.login.form.LoginUser;
import com.company.da.project.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @author: jasmine
*/
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping("/login")
public Response<String> login(@RequestBody LoginUser loginUser) {
return Response.success(loginService.login(loginUser));
}
@GetMapping("/exit")
public Response<String> logout() {
return Response.success(loginService.logout());
}
@PostMapping("/zhuce")
public Response<String> register(@RequestBody LoginUser loginUser) {
return Response.success(loginService.register(loginUser));
}
}
LoginService对登陆、退出、注册的逻辑实现
import com.company.da.project.common.RedisConstant;
import com.company.da.project.common.exception.PlatformException;
import com.company.da.project.controller.login.form.LoginUser;
import com.company.da.project.dto.JwtUser;
import com.company.da.project.dto.RedisUser;
import com.company.da.project.util.JsonUtils;
import com.company.da.project.util.RedisUtil;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Calendar;
import java.util.Date;
/**
* @author: jasmine
*/
@Service
public class LoginService {
@Autowired
private RedisUtil redisUtil;
@Autowired
private UserService userService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
public String login(LoginUser loginUser) {
String userName = loginUser.getUserName();
String password = loginUser.getPassword();
//1、authenticationManager对象验证usernamePasswordAuthenticationToken中用户名和密码
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userName, password);
//底层调用UserDetialServiceImpl中的loadUserByUsername查询用户信息,并进行密码比对
//如果用户名或者密码错误,会在这个函数中校验返回,不继续执行
Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
//登陆失败 TODO 走不到这个方法
if (null == authenticate) {
throw new PlatformException("登陆失败");
}
//2、如果登陆成功,获取JwtUser对象
JwtUser jwtUser = (JwtUser) authenticate.getPrincipal();
//3、生成jwt,并将jwt返回
String userId = jwtUser.getUserId();
// 生成token过程
//String token = genToken(userId);
//将完整user对象存入redis
redisUtil.set(RedisConstant.ACS_TOKEN + userId, JsonUtils.toString(new RedisUser(jwtUser)), RedisConstant.ACS_TOKEN_TTL);
return userId;
}
//生成token过程
private String genToken(String userId) {
Calendar calendar = Calendar.getInstance();
Date now = calendar.getTime();
// 设置签发时间
calendar.setTime(new Date());
// 设置过期时间
calendar.add(Calendar.MINUTE, 5);// 5分钟
Date time = calendar.getTime();
String token = Jwts.builder()
.setSubject(userId)
.setIssuedAt(now)//签发时间
.setExpiration(time)//过期时间
.signWith(SignatureAlgorithm.HS512, "spring-security-@Jwt!&Secret^#") //采用什么算法是可以自己选择的,不一定非要采用HS512
.compact();
return token;
}
public String logout() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
JwtUser jwtUser = (JwtUser) authentication.getPrincipal();
String userId = jwtUser.getUserId();
boolean del = redisUtil.del(RedisConstant.ACS_TOKEN + userId);
return del ? "success" : "fail";
}
public String register(LoginUser loginUser) {
String userName = loginUser.getUserName();
String encodePwd = bCryptPasswordEncoder.encode(loginUser.getPassword());
return userService.updateOrInitUser(userName, encodePwd);
}
}