【实战运用】SpringSecurity+Redis+Jwt实现用户认证授权
Spring Security是一个强大且灵活的身份验证和访问控制框架,用于Java应用程序。它是基于Spring框架的一个子项目,旨在为应用程序提供安全性。Spring Security致力于为Java应用程序提供认证和授权功能。开发者可以轻松地为应用程序添加强大的安全性,以满足各种复杂的安全需求。
介绍
Spring Security是一个强大且灵活的身份验证和访问控制框架,用于Java应用程序。它是基于Spring框架的一个子项目,旨在为应用程序提供安全性。
Spring Security致力于为Java应用程序提供认证和授权功能。开发者可以轻松地为应用程序添加强大的安全性,以满足各种复杂的安全需求。
SpringSecurity完整流程
JwtAuthenticationTokenFilter: 这里是我们自己定义的过滤器,主要负责放行不携带token的请求(如注册或登录请求),并对携带token的请求设置授权信息
UsernamePasswordAuthenticationFilter: 负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要由它负责
ExceptionTranslationFilter: 处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException
FilterSecurityInterceptor: 负责权限校验的过滤器。
一般认证工作流程
Authentication接口: 它的实现类表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager接口: 定义了认证Authentication的方法
UserDetailsService接口: 加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails接口: 提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
数据库
数据库的采用**RBAC权限模型(基于角色的权限控制)**进行设计。
RBAC至少需要三张表:用户表–角色表–权限表(多对多的关系比较合理)
- 用户表(user):存储用户名、密码等基础信息,进行登录校验
- 角色表(role):对用户的角色进行分配
- 权限表(menu):存储使用不同功能所需的权限
注册流程
配置匿名访问
在配置类中允许注册请求可以匿名访问
编写实现类
registerDTO中存在字符串roleId和实体类user,先取出user判断是否存在相同手机号。若该手机号没有注册过用户,对密码进行加密后即可将用户存入数据库。
创建register方法映射,保存用户的同时也要将roleId一并存入关系表中,使用户获得对应角色。如下图。
@Override
public Result register(RegisterDTO registerDTO) {
// 获取Map中的数据
User user = registerDTO.getUser();
String roleId = registerDTO.getRoleId();
// 判断是否存在相同手机号
User dataUser = lambdaQuery()
.eq(User::getUserPhone, user.getUserPhone()).one();
if (!Objects.isNull(dataUser)) {
return Result.fail("该手机号已注册过用户,请勿重复注册");
}
// 密码加密
user.setUserPassword(passwordEncoder
.encode(user.getUserPassword()));
// 将用户及对应角色存入数据库
save(user);
userMapper.register(user.getUserPhone(), roleId);
return Result.ok("注册成功");
}
登录流程
配置匿名访问
在配置类中允许登录请求可以匿名访问
调用UserDetailsServiceImpl
登录流程一般对应认证工作流程
@Resource
private AuthenticationManager authenticationManager;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private UserMapper userMapper;
@Override
public Result login(User user) {
//AuthenticationManager 进行用户认证,校验手机号和密码是否正确
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserPhone(), user.getUserPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//认证失败给出提示
if (Objects.isNull(authenticate)) {
throw new RuntimeException("用户名或密码错误");
}
//认证通过,生成jwt并返回
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getUserId();
String jwtToken = JwtUtil.createToken(userId);
Map<String, String> map = new HashMap<>();
stringRedisTemplate.opsForValue()
.set(LOGIN_CODE_KEY + userId, JSONUtil.toJsonStr(loginUser));
map.put("token", jwtToken);
return Result.ok(map);
}
先看这段代码: UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserPhone(), user.getUserPassword());
这里先用用户手机号和密码生成UsernamePasswordAuthenticationToken
再看这段代码:Authentication authenticate = authenticationManager.authenticate(authenticationToken);
利用authenticate调用自定义实现类UserDetailsServiceImpl,根据用户名判断用户是否存在(对应认证流程的1、2、3、4)
实现UserDetailsServiceImpl
由于试下的是UserDetailsService接口,所以必须实现其方法loadUserByUsername(根据用户名查询数据库是否存在)这里我传入的是手机号。数据库中若存在用户,则返回UserDetails对象(这里的权限信息暂且不看,对应认证流程的5、5.1、5.2、6)
UserDetails对象返回后,authenticate方法会默认通过PasswordEncoder比对UserDetails与Authentication的密码是否相同。因为UserDetails是通过自定义实现类从数据库中查询出的user对象,而Authentication相当于是用户输入的用户名和密码,也就可以理解为通过前面自定义实现类利用用户名查询到用户后,再看这个用户的密码是否正确。如果用户名或密码不正确,authenticate将会为空,则抛出异常信息。(对应认证流程的7)
由于这里的登录流程不涉及8,9,10,所以不再叙述。
在剩下的代码中我们利用用userId生成了jwt的令牌token,将其存入Redis中并返回token给前端。
登出流程
编写过滤器
除login、register请求外的所有请求都需要携带token才能访问,因此需要设计token拦截器代码,如下。
对于不携带token的请求(如登录/注册)直接放行;对于携带token的请求先判断该用户是否登录,即redis中是否存在相关信息,若存在,将用户授权信息存入SecurityContextHolder,方便用户授权,最后直接放行。
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
// 没有token,放行
filterChain.doFilter(request, response);
return;
}
// 解析token
String userId = null;
try {
userId = JwtUtil.parseJwt(token);
} catch (Exception e) {
e.printStackTrace();
System.out.println("token非法:" + e);
}
// 从redis中获取用户信息
String userJson = stringRedisTemplate
.opsForValue().get(LOGIN_CODE_KEY + userId);
LoginUser loginUser = JSONUtil.toBean(userJson, LoginUser.class);
if (Objects.isNull(loginUser)) {
throw new RuntimeException("用户未登录");
}
// 存入SecurityContextHolder,设置用户授权信息
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 放行
filterChain.doFilter(request, response);
}
}
此外,还需将token拦截器设置在过滤器UsernamePasswordAuthenticationFilter的前面。
编写实现类
@Override
public Result logout() {
// 获取SecurityContextHolder中的用户id
UsernamePasswordAuthenticationToken authentication =
(UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
String userId = loginUser.getUser().getUserId();
// 删除redis中的值
stringRedisTemplate
.delete(LOGIN_CODE_KEY + userId);
return Result.ok("注销成功");
}
获取SecurityContextHolder中的用户id后,删除redis中存储的值,即登出成功。
授权流程
确保实现类正确编写:
@Data
@NoArgsConstructor
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;
}
@JsonIgnore
private List<SimpleGrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (authorities != null) {
return authorities;
}
// 把permissions中String类型的权限信息封装成SimpleGrantedAuthority对象
authorities = permissions.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return authorities;
}
@Override
public String getPassword() {
return user.getUserPassword();
}
@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;
}
}
在token拦截器中,我们添加了这段代码。
// 存入SecurityContextHolder,设置用户授权信息
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
这样非登录/注册请求都会被设置授权信息。
为对应接口添加注解@PreAuthorize,就会检验该请求是否存在相关请求。
完整代码
config类
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Resource
private AccessDeniedHandlerImpl accessDeniedHandler;
@Resource
private AuthenticationEntryPointImpl authenticationEntryPoint;
@Bean
public PasswordEncoder passwordEncoder() {
// 实例化PasswordEncoder
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/user/login", "/user/register").anonymous()
.anyRequest().authenticated();
// 添加过滤器
http
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 配置异常处理器
http
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
// 允许跨域
http.cors();
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
// 配置身份验证管理器
return authenticationConfiguration.getAuthenticationManager();
}
}
controller类
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private IUserService userService;
@PostMapping("/login")
public Result login(@RequestBody User user) {
return userService.login(user);
}
@GetMapping("/logout")
public Result logout() {
return userService.logout();
}
@PostMapping("/register")
public Result register(@RequestBody RegisterDTO registerDTO) {
return userService.register(registerDTO);
}
}
dto类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RegisterDTO {
private User user;
private String roleId;
}
/**
* @author modox
* @date 2023年6月1日
* @description 封装结果后返回
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
public static final Integer SUCCESS_CODE = 200; // 访问成功状态码
public static final Integer TOKEN_ERROR = 400; // Token错误状态码
public static final Integer ERROR_CODE = 500; // 访问失败状态码
private Integer status; // 状态码
private String msg; // 提示消息
private Object data = null;
public Result(Integer status, String msg) {
this.status = status;
this.msg = msg;
}
public static Result ok(Integer status,String msg,Object data){
return new Result(status,msg,data);
}
public static Result ok(String msg,Object data){
return new Result(SUCCESS_CODE,msg,data);
}
public static Result ok(Object data){
return new Result(SUCCESS_CODE,"操作成功",data);
}
public static Result ok(){
return new Result(SUCCESS_CODE,"操作成功",null);
}
public static Result fail(Integer status,String msg){
return new Result(status,msg);
}
public static Result fail(String msg){
return new Result(ERROR_CODE,msg);
}
public static Result fail(){
return new Result(ERROR_CODE,"操作失败");
}
public static Map<String,Object> ok(Map<String,Object> map){
map.put("status",SUCCESS_CODE);
map.put("msg","查询成功");
return map;
}
public static Map<String,Object> ok(PageInfo pageInfo){
Map<String,Object> map = new HashMap<>();
map.put("status",SUCCESS_CODE);
map.put("msg","查询成功");
map.put("count",pageInfo.getTotal());
map.put("data",pageInfo.getList());
return map;
}
}
entity类
UserDetails的实现类
@Data
@NoArgsConstructor
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;
}
@JsonIgnore
private List<SimpleGrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (authorities != null) {
return authorities;
}
// 把permissions中String类型的权限信息封装成SimpleGrantedAuthority对象
authorities = permissions.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return authorities;
}
@Override
public String getPassword() {
return user.getUserPassword();
}
@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;
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName(value = "grd_menu")
public class Menu {
@TableId
private String menuId;
private String menuName;
private String menuPerms;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName(value = "grd_user")
public class User {
@TableId(type = IdType.ASSIGN_ID)
private String userId;
private String userName;
private Integer userSex;
private String userPhone;
private String userPassword;
private String userSchool;
private Byte[] userImage;
}
filter类
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
// 没有token,放行
filterChain.doFilter(request, response);
return;
}
// 解析token
String userId = null;
try {
userId = JwtUtil.parseJwt(token);
} catch (Exception e) {
e.printStackTrace();
System.out.println("token非法:" + e);
}
// 从redis中获取用户信息
String userJson = stringRedisTemplate
.opsForValue().get(LOGIN_CODE_KEY + userId);
LoginUser loginUser = JSONUtil.toBean(userJson, LoginUser.class);
if (Objects.isNull(loginUser)) {
throw new RuntimeException("用户未登录");
}
// 存入SecurityContextHolder,设置用户授权信息
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 放行
filterChain.doFilter(request, response);
}
}
handler类
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
Result result = new Result(HttpStatus.FORBIDDEN.value(), "您的权限不足");
String json = JSONUtil.toJsonStr(result);
// 处理异常
WebUtils.renderString(response, json);
}
}
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
Result result = new Result(HttpStatus.UNAUTHORIZED.value(), "用户认证失败");
String json = JSONUtil.toJsonStr(result);
// 处理异常
WebUtils.renderString(response, json);
}
}
service实现类
@Service
public class UserDetailsServiceImpl extends ServiceImpl<UserMapper, User> implements UserDetailsService {
@Resource
private MenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String userPhone) throws UsernameNotFoundException {
//根据用户名查询用户信息
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("user_phone", userPhone);
User user = getOne(wrapper);
//若数据库中不存在用户
if (Objects.isNull(user)) {
throw new RuntimeException("该手机号未注册");
}
// 根据用户查询权限信息 添加到LoginUser中
List<String> list = menuMapper.selectPermsByUserPhone(user.getUserPhone());
// 封装成UserDetails对象返回
return new LoginUser(user, list);
}
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Resource
private AuthenticationManager authenticationManager;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private UserMapper userMapper;
@Override
public Result login(User user) {
//AuthenticationManager 进行用户认证,校验手机号和密码是否正确
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserPhone(), user.getUserPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//认证失败给出提示
if (Objects.isNull(authenticate)) {
throw new RuntimeException("用户名或密码错误");
}
//认证通过,生成jwt并返回
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getUserId();
String jwtToken = JwtUtil.createToken(userId);
Map<String, String> map = new HashMap<>();
stringRedisTemplate.opsForValue()
.set(LOGIN_CODE_KEY + userId, JSONUtil.toJsonStr(loginUser));
map.put("token", jwtToken);
return Result.ok(map);
}
@Override
public Result logout() {
// 获取SecurityContextHolder中的用户id
UsernamePasswordAuthenticationToken authentication =
(UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
String userId = loginUser.getUser().getUserId();
// 删除redis中的值
stringRedisTemplate
.delete(LOGIN_CODE_KEY + userId);
return Result.ok("注销成功");
}
@Override
public Result register(RegisterDTO registerDTO) {
// 获取Map中的数据
User user = registerDTO.getUser();
String roleId = registerDTO.getRoleId();
// 判断是否存在相同手机号
User dataUser = lambdaQuery()
.eq(User::getUserPhone, user.getUserPhone()).one();
if (!Objects.isNull(dataUser)) {
return Result.fail("该手机号已注册过用户,请勿重复注册");
}
// 密码加密
user.setUserPassword(passwordEncoder
.encode(user.getUserPassword()));
// 将用户及对应角色存入数据库
save(user);
userMapper.register(user.getUserPhone(), roleId);
return Result.ok("注册成功");
}
}
utils类
public class JwtUtil {
// token失效:24小时
public static final String token = "token";
public static final long EXPIPE = 1000 * 60 * 60 * 10;
public static final String APP_SECRET = "modox@ukc8BDbRigUDaY6pZFfWus2jZWLPHO";
/**
* 根据传入的用户Id生成token
* @param userId
* @return JWT规则生成的token
*/
public static String createToken(String userId) {
String JwtToken = Jwts.builder()
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
.setSubject("grd_user")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIPE))
.claim("userId", userId)
.signWith(SignatureAlgorithm.HS256, APP_SECRET)
.compact();
return JwtToken;
}
/**
* 验证token是否有效
* @param jwtToken token字符串
* @return 如果token有效返回true,否则false
*/
public static boolean checkToken(String jwtToken) {
try {
if (!StringUtils.hasText(jwtToken))
return false;
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 根据token获取User信息
* @param jwtToken token字符串
* @return 解析token获得的user对象
*/
public static String parseJwt(String jwtToken) {
//验证token
if (checkToken(jwtToken)) {
Claims claims = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken).getBody();
return claims.get("userId").toString();
}else {
throw new RuntimeException("超时或不合法token");
}
}
}
public class RedisConstants {
public static final String LOGIN_CODE_KEY = "login:code:";
public static final Long LOGIN_CODE_TTL = 2L;
public static final String LOGIN_USER_KEY = "login:token:";
public static final Long LOGIN_USER_TTL = 36000L;
}
public class WebUtils {
/**
* 将字符串渲染到客户端
* @param response
* @param string
* @return
*/
public static String renderString(HttpServletResponse response, String string) {
try {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)