一、认证
1. 自定义组件
(1)UserDetails自定义,实现用户登录方法;
public interface UserDetailsService extends org.springframework.security.core.userdetails.UserDetailsService {
/**
* 根据用户名获取用户对象(获取不到直接抛异常)
*/
@Override
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getByUsername(username);
if(null == sysUser) {
throw new UsernameNotFoundException("用户名不存在!");
}
if(sysUser.getStatus().intValue() == 0) {
throw new RuntimeException("账号已停用");
}
return new CustomUser(sysUser, Collections.emptyList());
}
}
(2)自定义passwordEncoder
@Component
public class CustomMd5PasswordEncoder implements PasswordEncoder {
public String encode(CharSequence rawPassword) {
return MD5.encrypt(rawPassword.toString());
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
}
}
(3)添加CustomUser对象
import com.lewang.model.system.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
/**
* @Description :
* @date :2023/10/26 21:06
*/
public class CustomUser extends User {
/**
* 我们自己的用户实体对象,要调取用户信息时直接获取这个实体对象。(这里我就不写get/set方法了)
*/
private SysUser sysUser;
public CustomUser(SysUser sysUser, Collection<? extends GrantedAuthority> authorities) {
super(sysUser.getUsername(), sysUser.getPassword(), authorities);
this.sysUser = sysUser;
}
public SysUser getSysUser() {
return sysUser;
}
public void setSysUser(SysUser sysUser) {
this.sysUser = sysUser;
}
}
2. 具体核心组件
(1)登录filter,判断用户名和密码是否正确,生成token;
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
//构造方法
public TokenLoginFilter(AuthenticationManager authenticationManager){
this.setAuthenticationManager(authenticationManager);
this.setPostOnly(false);
//指定登录接口提交方式
this.setRequiresAuthenticationRequestMatcher(
new AntPathRequestMatcher("/admin/system/index/login","POST"));
}
//登录认证
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
LoginVo loginVo = new ObjectMapper().readValue(request.getInputStream(), LoginVo.class);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
//调用方式认证
return this.getAuthenticationManager().authenticate(authenticationToken);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//登录成功
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//获取用户
CustomUser customUser = (CustomUser) authResult.getPrincipal();
//生成token
String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());
//返回
Map<String, Object> map = new HashMap<>();
map.put("token", token);
ResponseUtil.out(response, Result.ok(map));
}
//登录失败
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
ResponseUtil.out(response,Result.build(null, ResultCodeEnum.LOGIN_ERROR));
}
}
(2)认证解析token
从请求头中获取token,解析token获取用户信息,保存到上下文对象中
public class TokenAuthenticationFilter extends OncePerRequestFilter {
public TokenAuthenticationFilter(){}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
//如果是登录接口,直接放行
if("/admin/system/index/login".equals(request.getRequestURI())) {
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
if (authentication != null){
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request,response);
}else {
ResponseUtil.out(response, Result.build(null, ResultCodeEnum.LOGIN_ERROR));
}
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
//从请求头中获取token
String token = request.getHeader("token");
if (!StringUtils.isEmpty(token)){
String username = JwtHelper.getUsername(token);
if (!StringUtils.isEmpty(username)){
return new UsernamePasswordAuthenticationToken(username,null, Collections.emptyList());
}
}
return null;
}
}
(3)配置用户认证
@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private CustomMd5PasswordEncoder customMd5PasswordEncoder;
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护
http
//关闭csrf跨站请求伪造
.csrf().disable()
// 开启跨域以便前端调用接口
.cors().and()
.authorizeRequests()
// 指定某些接口不需要通过验证即可访问。登陆接口肯定是不需要认证的
.antMatchers("/admin/system/index/login").permitAll()
// 这里意思是其它所有接口需要认证才能访问
.anyRequest().authenticated()
.and()
//TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面,这样做就是为了除了登录的时候去查询数据库外,其他时候都用token进行认证。
.addFilterBefore(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilter(new TokenLoginFilter(authenticationManager()));
//禁用session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 指定UserDetailService和加密器
auth.userDetailsService(userDetailsService)
.passwordEncoder(customMd5PasswordEncoder);
}
/**
* 配置哪些请求不拦截
* 排除swagger相关请求
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/favicon.ico","/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/doc.html");
}
}
当没有登录时访问接口会提示认证失败
认证成功后返回token
二、授权
(1)在获取用户信息时同时获取用户权限信息
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Autowired
private SysMenuService sysMenuService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getByUsername(username);
if(null == sysUser) {
throw new UsernameNotFoundException("用户名不存在!");
}
if(sysUser.getStatus().intValue() == 0) {
throw new RuntimeException("账号已停用");
}
//根据userid查询用户操作权限数据
List<String> userPerms = sysMenuService.findUserPermsByUserId(sysUser.getId());
//创建list集合,封装权限数据
List<SimpleGrantedAuthority> authList = new ArrayList<>();
//查询遍历集合
for(String perms : userPerms){
authList.add(new SimpleGrantedAuthority(perms.trim()));
}
return new CustomUser(sysUser, authList);
}
}
(2)修改TokenLoginFilter,增加权限数据;获取当前登录用户的权限数据,把权限数据保存到redis中
//登录成功
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//获取用户
CustomUser customUser = (CustomUser) authResult.getPrincipal();
//生成token
String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());
//获取用户权限信息保存redis中
redisTemplate.opsForValue().set(customUser.getUsername(), JSON.toJSONString(customUser.getAuthorities()));
//返回
Map<String, Object> map = new HashMap<>();
map.put("token", token);
ResponseUtil.out(response, Result.ok(map));
}
(3)修改TokenAuthenticationFilter,认证从redis中获取权限数据
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private RedisTemplate redisTemplate;
public TokenAuthenticationFilter(RedisTemplate redisTemplate){
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
//如果是登录接口,直接放行
if("/admin/system/index/login".equals(request.getRequestURI())) {
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
if (authentication != null){
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request,response);
}else {
ResponseUtil.out(response, Result.build(null, ResultCodeEnum.LOGIN_ERROR));
}
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
//从请求头中获取token
String token = request.getHeader("token");
if (!StringUtils.isEmpty(token)){
String username = JwtHelper.getUsername(token);
if (!StringUtils.isEmpty(username)){
//使用username从redis中获取权限数据
String authString = (String) redisTemplate.opsForValue().get(username);
//把从redis中获取的权限字符串转换成数据权限集合类型List<SimpleGrantedAuthority>
if (!StringUtils.isEmpty(authString)){
List<Map> mapList = JSON.parseArray(authString, Map.class);
System.out.println(mapList);
List<SimpleGrantedAuthority> authList = new ArrayList<>();
for(Map map : mapList){
String authority = (String) map.get("authority");
authList.add(new SimpleGrantedAuthority(authority));
}
return new UsernamePasswordAuthenticationToken(username,null, authList);
}else {
return new UsernamePasswordAuthenticationToken(username,null, Collections.emptyList());
}
}
}
return null;
}
}
(4)修改配置类
使用注解开启方法的认证机制
@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启方法的权限控制
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private CustomMd5PasswordEncoder customMd5PasswordEncoder;
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护
http
//关闭csrf跨站请求伪造
.csrf().disable()
// 开启跨域以便前端调用接口
.cors().and()
.authorizeRequests()
// 指定某些接口不需要通过验证即可访问。登陆接口肯定是不需要认证的
.antMatchers("/admin/system/index/login").permitAll()
// 这里意思是其它所有接口需要认证才能访问
.anyRequest().authenticated()
.and()
//TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面,这样做就是为了除了登录的时候去查询数据库外,其他时候都用token进行认证。
.addFilterBefore(new TokenAuthenticationFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class)
.addFilter(new TokenLoginFilter(authenticationManager(),redisTemplate));
//禁用session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 指定UserDetailService和加密器
auth.userDetailsService(userDetailsService)
.passwordEncoder(customMd5PasswordEncoder);
}
/**
* 配置哪些请求不拦截
* 排除swagger相关请求
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/favicon.ico","/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/doc.html");
}
}
(5)在接口上添加权限注解
@ApiOperation("添加角色")
@PostMapping("save")
@PreAuthorize("hasAuthority('bnt.sysRole.add')")
public Result save(@RequestBody SysRole role) {
//调用service的方法
boolean is_success = sysRoleService.save(role);
if(is_success) {
return Result.ok();
} else {
return Result.fail();
}
}