1、SpringSecurity流程图
2、导入坐标
org.springframework.boot spring-boot-starter-security
io.jsonwebtoken jjwt 0.9.1
3、SpringSecurity的配置类
package com.springsecuritylearning.config;import com.springsecuritylearning.filter.DoFilterInternal;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate DoFilterInternal filterInternal;@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过的Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()//对于该接口放行.antMatchers("/auth/user/**").permitAll()//其余接口拦截.anyRequest().authenticated();http.addFilterBefore(filterInternal, UsernamePasswordAuthenticationFilter.class);}@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}}
4、根据流程图可以看出用户登录接口,将用户名和密码进行提交,所以先写登录接口(流程图第1步)
/*** 登录验证*/
@RestController
@RequestMapping("/auth/user")
@CrossOrigin
@Slf4j
public class UserController {@Autowiredprivate UserServiceImpl userService;/*** 用户登录* @param user* @return*/@PostMapping("/login")public R login(User user){System.out.println(user);String token = userService.login(user);HashMap map=new HashMap<>();map.put("token",token);return R.ok(map).setCode(200);}
}
5、创建login接口的实现类(流程图第2、3步,第4步自动完成,最后获取到Authentication为第9步)
package com.xuechengplusauth.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xuechengplusauth.domain.User;
import com.xuechengplusauth.mapper.UserMapper;
import com.xuechengplusauth.security.LoginUser;
import com.xuechengplusauth.service.UserService;
import com.xuechengpluscommon.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
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.stereotype.Service;
import org.springframework.util.ObjectUtils;/**
* @author 卿十三
* @description 针对表【xc_user】的数据库操作Service实现
* @createDate 2023-02-08 14:48:36
*/
@Service
public class UserServiceImpl extends ServiceImplimplements UserService{@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate AuthenticationManager authenticationManager;@Overridepublic String login(User user) {//将请求的信息封装到Authentication,实现类为UsernamePasswordAuthenticationTokenUsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword());Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);//判断Authentication是否通过认证,如果为空则没有通过PasswordEncoder的认证,说明账号密码错误if(ObjectUtils.isEmpty(authenticate)){throw new RuntimeException("未通过认证");}//从Authentication获取信息LoginUser loginUser = (LoginUser) authenticate.getPrincipal();//从loginUser类获取User信息User loginUserUser = loginUser.getUser();String id = loginUserUser.getId();//将用户Id保存到redisredisTemplate.opsForValue().set(id,loginUser);//将用户Id转成JWT然后返回String token = JwtUtils.createToken(id);return token;}
}
6、经过自动认证之后来到UserDetailsService进行手动填充信息,首先创建LoginUser类实现UserDetails接口,用来作为loadUserByUsername类的返回值(流程图5、6、7、8步)
package com.xuechengplusauth.security;import com.alibaba.fastjson.annotation.JSONField;
import com.xuechengplusauth.domain.User;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import java.util.ArrayList;
import java.util.Collection;
import java.util.List;@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {//存储用户信息private User user;//存储权限信息private List authList;//优化getAuthorities类,避免多次重复调用@JSONField(serialize = false)private List authorities;public LoginUser(User user,List authList){this.user=user;this.authList=authList;}@Overridepublic Collection extends GrantedAuthority> getAuthorities() {if(authorities!=null){return authorities;}else{authorities=new ArrayList<>();for (String s : authList) {SimpleGrantedAuthority simpleGrantedAuthority=new SimpleGrantedAuthority(s);authorities.add(simpleGrantedAuthority);}return authorities;}}//用户密码,通过user.getPassword()获取的密码来通过PassWordEncoder加密后和数据库的密码进行校验@Overridepublic String getPassword() {return user.getPassword();}//用户账号@Overridepublic String getUsername() {return user.getUsername();}//返回true意味着账号未过期@Overridepublic boolean isAccountNonExpired() {return true;}//返回true意味着账号未锁定@Overridepublic boolean isAccountNonLocked() {return true;}//返回true意味着凭证未过期@Overridepublic boolean isCredentialsNonExpired() {return true;}//返回true意味着可以使用@Overridepublic boolean isEnabled() {return true;}
}
7、创建UserDetailsImpl类实现UserDetailsService接口,通过重写loadUserByUsername方法实现自定义校验(流程图5、6、7、8步)
package com.xuechengplusauth.security.Impl;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xuechengplusauth.domain.Menu;
import com.xuechengplusauth.domain.Permission;
import com.xuechengplusauth.domain.User;
import com.xuechengplusauth.domain.UserRole;
import com.xuechengplusauth.mapper.MenuMapper;
import com.xuechengplusauth.mapper.PermissionMapper;
import com.xuechengplusauth.mapper.UserMapper;
import com.xuechengplusauth.mapper.UserRoleMapper;
import com.xuechengplusauth.security.LoginUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;import java.util.ArrayList;
import java.util.List;
@Service
public class UserDetailsImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate UserRoleMapper userRoleMapper;@Autowiredprivate PermissionMapper permissionMapper;@Autowiredprivate MenuMapper menuMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//根据输入的账号username从数据库查询到用户LambdaQueryWrapper queryWrapper=new LambdaQueryWrapper<>();queryWrapper.eq(User::getUsername,username);//获取查到的用户User user = userMapper.selectOne(queryWrapper);//如果为空用户不存在if(ObjectUtils.isEmpty(user)){throw new RuntimeException("用户不存在");}//从数据库查询该用户所拥有的权限信息//创建权限集合,用来存储权限List authList=new ArrayList<>();String id = user.getId();LambdaQueryWrapper queryWrapper1=new LambdaQueryWrapper<>();queryWrapper1.eq(UserRole::getUserId,id);UserRole userRole = userRoleMapper.selectOne(queryWrapper1);String roleId = userRole.getRoleId();LambdaQueryWrapper queryWrapper2=new LambdaQueryWrapper<>();queryWrapper2.eq(Permission::getRoleId,roleId);List permissions = permissionMapper.selectList(queryWrapper2);for (Permission permission : permissions) {String menuId = permission.getMenuId();Menu menu = menuMapper.selectById(menuId);String code = menu.getCode();authList.add(code);}//将用户信息和权限信息封装到loginUser中LoginUser loginUser=new LoginUser(user,authList);//返回loginUserreturn loginUser;}
}
8、编写拦截器将Authentication保存到上下文中,通过继承OncePerRequestFilter类实现doFilterInternal接口实现拦截器的功能(流程图第10步)
package com.xuechengplusauth.filter;import com.xuechengplusauth.security.LoginUser;
import com.xuechengpluscommon.utils.JwtUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
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;
import java.util.Collection;@Component
public class DoFilterInternal extends OncePerRequestFilter {@Autowiredprivate RedisTemplate redisTemplate;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//获取请求头的token信息String token = request.getHeader("token");//如果为空,则放行if(StringUtils.isEmpty(token)){filterChain.doFilter(request,response);return;}//JWT进行解密获取Token信息String tokenInfo = JwtUtils.getTokenInfo(token);//从数据库查询token信息中的用户LoginUser loginUser = (LoginUser) redisTemplate.opsForValue().get(tokenInfo);//如果用户存在说明已经过认证,如果不存在,说明未经过认证if(ObjectUtils.isEmpty(loginUser)){throw new RuntimeException("用户不存在");}//用户已经进行认证的前提下,获取UserDetails的信息,从其中获取认证用户的权限和信息Collection extends GrantedAuthority> authorities = loginUser.getAuthorities();//通过SecurityContextHolder.getContext().setAuthentication方法将Authentication保存到上下文UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(loginUser,null,authorities);SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);filterChain.doFilter(request,response);}
}
9、测试登录
设置token未空,在Security配类开启登录接口的放行,设置JWT的存活时间和Redis的存活时间为一小时
返回值为token
10、测试其他模块,该模块已导入SpringSecurity模块的包
带有Token进行接口测试(成功访问)
不带Token进行接口测试(禁止访问)
11、权限功能测试
启动类添加注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
在Controller接口添加注解@PreAuthorize
@RequestMapping("/test1")@PreAuthorize("hasAuthority('p1')")public String r(){return "访问资源R1";}@RequestMapping("/test2")@PreAuthorize("hasAuthority('p2')")public String test(){return "访问资源test";}
添加三种权限p1,p2,p3
authList.add("p1");
authList.add("p2");
authList.add("p3");
获取当前用户的权限信息
xc_sysmanager
xc_sysmanager_user
xc_sysmanager_user_add
xc_sysmanager_user_edit
xc_sysmanager_user_view
xc_sysmanager_user_delete
xc_sysmanager_doc
xc_sysmanager_log
xc_teachmanager_course
xc_teachmanager_course_add
xc_teachmanager_course_base
p1
p2
p3
测试p1,p2接口访问(成功访问)
删除P1,2,p3权限,当前用户拥有的权限
xc_sysmanager
xc_sysmanager_user
xc_sysmanager_user_add
xc_sysmanager_user_edit
xc_sysmanager_user_view
xc_sysmanager_user_delete
xc_sysmanager_doc
xc_sysmanager_log
xc_teachmanager_course
xc_teachmanager_course_add
xc_teachmanager_course_base
测试接口(访问失败)
上一篇:Linux之文本搜索命令
下一篇:一文详解网络安全事件的防护与响应