1、SpringSecurity流程图
2、导入坐标
<!-- spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- jjwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
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 {
@Autowired
private DoFilterInternal filterInternal;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected 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);
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
4、根据流程图可以看出用户登录接口,将用户名和密码进行提交,所以先写登录接口(流程图第1步)
/**
* 登录验证
*/
@RestController
@RequestMapping("/auth/user")
@CrossOrigin
@Slf4j
public class UserController {
@Autowired
private UserServiceImpl userService;
/**
* 用户登录
* @param user
* @return
*/
@PostMapping("/login")
public R login(User user){
System.out.println(user);
String token = userService.login(user);
HashMap<String,String> 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 ServiceImpl<UserMapper, User>
implements UserService{
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private AuthenticationManager authenticationManager;
@Override
public String login(User user) {
//将请求的信息封装到Authentication,实现类为UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken 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保存到redis
redisTemplate.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<String> authList;
//优化getAuthorities类,避免多次重复调用
@JSONField(serialize = false)
private List<SimpleGrantedAuthority> authorities;
public LoginUser(User user,List<String> authList){
this.user=user;
this.authList=authList;
}
@Override
public 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加密后和数据库的密码进行校验
@Override
public String getPassword() {
return user.getPassword();
}
//用户账号
@Override
public String getUsername() {
return user.getUsername();
}
//返回true意味着账号未过期
@Override
public boolean isAccountNonExpired() {
return true;
}
//返回true意味着账号未锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
//返回true意味着凭证未过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//返回true意味着可以使用
@Override
public 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 {
@Autowired
private UserMapper userMapper;
@Autowired
private UserRoleMapper userRoleMapper;
@Autowired
private PermissionMapper permissionMapper;
@Autowired
private MenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据输入的账号username从数据库查询到用户
LambdaQueryWrapper<User> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUsername,username);
//获取查到的用户
User user = userMapper.selectOne(queryWrapper);
//如果为空用户不存在
if(ObjectUtils.isEmpty(user)){
throw new RuntimeException("用户不存在");
}
//从数据库查询该用户所拥有的权限信息
//创建权限集合,用来存储权限
List<String> authList=new ArrayList<>();
String id = user.getId();
LambdaQueryWrapper<UserRole> queryWrapper1=new LambdaQueryWrapper<>();
queryWrapper1.eq(UserRole::getUserId,id);
UserRole userRole = userRoleMapper.selectOne(queryWrapper1);
String roleId = userRole.getRoleId();
LambdaQueryWrapper<Permission> queryWrapper2=new LambdaQueryWrapper<>();
queryWrapper2.eq(Permission::getRoleId,roleId);
List<Permission> 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);
//返回loginUser
return 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 {
@Autowired
private RedisTemplate redisTemplate;
@Override
protected 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
测试接口(访问失败)