详细流程和想了解源码
请跳转,大神写的很详细
(https://blog.csdn.net/yuanlaijike/article/details/80249235)
需要的依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
//spring security 核心包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
//热部署
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
// jwt 生成Token令牌
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
//小工具
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
//mybatis-plus 依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
// mybatis-plus 代码生成插件
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-tools</artifactId>
<version>2.0</version>
</dependency>
// 阿里云短信服务
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.1</version>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
//mysql驱动包
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
// redis 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
// JSON转换
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.1.37</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
//mybatis-plus 如果自己手写xml 一定要配置
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
数据库 就建简单的两张表测试就行了
用户表
权限表
各种工具类
统一返回的状态码
public interface RespCode {
Integer OK =20000;
Integer ERROR =20001;
}
正常请求统一返回数据格式
@Data
public class R {
private Integer code;
private boolean status;
private String message;
private Map<String, Object> data = new HashMap<>();
private R(){};
public static R ok() {
R r = new R();
r.code = RespCode.OK;
r.status = true;
r.message = "success";
return r;
}
public static R error() {
R r = new R();
r.code = RespCode.ERROR;
r.status = false;
r.message = "error";
return r;
}
public R message(String message) {
this.message = message;
return this;
}
public R code(Integer code) {
this.code = code;
return this;
}
public R data(String key, Object value) {
this.data.put(key, value);
return this;
}
public R data(Map<String, Object> data) {
this.data = data;
return this;
}
}
Security 各种处理器的返回格式
public class ResponeUtils {
public static void out(HttpServletResponse response,R r){
//传入自定义工具类R
ObjectMapper mapper = new ObjectMapper();
response.setStatus(HttpStatus.OK.value());
//统一返回的JSON数据
response.setContentType("application/json; charset=UTF-8");
PrintWriter writer = null;
try {
writer=response.getWriter();
//通过Response输出流 返回前端
writer.write(mapper.writeValueAsString(r));
} catch (IOException e) {
throw new CustomException(20001,r.getMessage());
}finally {
writer.close();
}
}
}
Jwt
public class JwtUtils {
//过期时间
private static final long EXPIREDTIME=60*60*24*1000;
// 密钥 (盐)
private static final String salt="chao";
public static String createToken(String username){
//传入username 成功token
String token = Jwts.builder().setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + EXPIREDTIME))
.signWith(SignatureAlgorithm.HS512, salt)
.compressWith(CompressionCodecs.GZIP)
.compact();
return token;
}
//通过Token 解析出 username
public static String getUsernameToken(String token){
String username = Jwts.parser().setSigningKey(salt).parseClaimsJws(token).getBody().getSubject();
return username;
}
}
密码工具类 (直接写在配置类中也可以)
@Configuration
public class PasswordEncoderConfig {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
异常工具类
自定义异常
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CustomException extends RuntimeException {
private Integer code;
private String message;
}
全局异常处理类
@RestController
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public R GlobalException(Exception e){
return R.error().message("执行了全局异常");
}
@ExceptionHandler(CustomException.class)
public R CustomException(CustomException e){
return R.error().code(e.getCode()).message(e.getMessage());
}
}
重写UserDetailsService方法 作用是 通过传入的username 从数据库中返回用户信息 给 UserDetails 设置值
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名从数据库中返回 user
User user = userService.gerUserByUsername(username);
//如果没有该用户 直接抛异常 交给failureHandler处理
if(user==null){
throw new InternalAuthenticationServiceException("用户不存在");
}
CumtomUserDetails userDetails=new CumtomUserDetails();
//通过用户的id 查找权限(就是数据中的路径)
List<String> permissons = userService.getPermissonByUserId(user.getId());
// 设值 然后返回
userDetails.setUsername(user.getUsername());
userDetails.setPasswrod(user.getPassword());
userDetails.setPermissons(permissons);
return userDetails;
}
}
重写 UserDetails
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CumtomUserDetails implements UserDetails {
private String username; //用户名
private String passwrod; //密码
private List<String> permissons; //权限 实际就是 路径的集合 ["/teacher","/student"]
private Boolean isAccountNonExpired; // 账号是否过期
private Boolean isAccountNonLocked; //账号是否锁定
private Boolean isCredentialsNonExpired; //密码是否过期
private Boolean isEnabled; // 是否被禁用
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//遍历 private List<String> permissons; 将路径字符串转换为 GrantedAuthority 对象
Collection<GrantedAuthority> authorities =new ArrayList<>();
for (String permisson : permissons) {
if(permisson==null) continue;
GrantedAuthority authority=new SimpleGrantedAuthority(permisson);
authorities.add(authority);
}
return authorities;
}
@Override
public String getPassword() {
return this.passwrod;
}
@Override
public String getUsername() {
return this.username;
}
//简单的统一返回true 实际可以通过UserDetailsService 从数据库中返回的
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
核心配置类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class CustomSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder; //密码加密
@Resource
private UserDetailsService userDetailsService; // //重写 userDetailsService 通过用户名去数据库加载密码
@Autowired
private CustomSuccessHandler customSuccessHandler; // 认证成功 处理 返回JSON数据
@Autowired
private CustomFailureHandler customFailureHandler; // 认证失败 处理 返回JSON数据
@Autowired
private CumtomAuthorizeFilter cumtomAuthorizeFilter; // 重写 OncePerRequestFilter 通过校验请求头中是否携带有令牌
@Autowired
private CustomAccessDeniedHandler customAccessDeniedHandler; // 登陆了 没有权限 是触发
@Autowired
private CustomAuthorizationEntryPoint authorizationEntryPoint;// 未登陆 触发
@Autowired
private CustomLayoutHandler customLayoutHandler; // 退出时 处理
@Autowired
private SmsSecurityConfig smsSecurityConfig; //短信登陆的配置类
@Override
protected void configure(HttpSecurity http) throws Exception {
http.apply(smsSecurityConfig) //短信登陆配置类 在下个博客补上
.and()
//必须放在UsernamePasswordAuthenticationFilter之前,如果请求头中替代有Token 就直接放行,没有的话 再进行用户名密码登陆
.addFilterBefore(cumtomAuthorizeFilter, UsernamePasswordAuthenticationFilter.class)
//异常处理器
.exceptionHandling()
.authenticationEntryPoint(authorizationEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler)
.and()
// 表单登陆 <input name="username"> <input name="password"> 如果有需求可以修改 通过 passwordParameter usernameParameter修改
.formLogin()
// 登陆的接口 自己定义
.loginProcessingUrl("/login")
// 成功处理器
.successHandler(customSuccessHandler)
// 失败处理器
.failureHandler(customFailureHandler)
.and()
// 授权请求
.authorizeRequests()
//permitAll 不需要任何授权 就可以访问
.antMatchers("/login","/logout","/sms/login","/sms/*").permitAll()
//authenticated 需要登陆了 才能进行访问
.antMatchers("/getUserInfo/*","/getMenu/*").authenticated()
//自定义权限控制器 其他所有请求 都要进行校验 返回True表示通过 返回false表示不能访问
.anyRequest().access("@rbacService.hasPermission(request,authentication)")
.and()
//退出处理 自定义url 和 处理器
.logout().logoutUrl("/logout")
.addLogoutHandler(customLayoutHandler)
.and()
//修改session策略 无状态应用 STATELESS 因为没用到session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//解决跨域访问
.cors().and().csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//配置 AuthenticationManager 放入自定义 userDetailsService 和 加密器
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
@Override
//放行静态资源 前后端分离了 不配置也没关系 如果使用swagger的话 可以配置一下
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/**",
"/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**"
);
}
//处理跨域问题
@Bean
CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowCredentials(true);
configuration.setAllowedOrigins(Arrays.asList("*")); //允许所有的跨域访问 有需要自己重写
configuration.setAllowedMethods(Arrays.asList("*")); //允许所有方法的请求 有需要自己重写
configuration.setAllowedHeaders(Arrays.asList("*")); //请求头 有需要自己重写
configuration.setMaxAge(Duration.ofHours(1));
source.registerCorsConfiguration("/**",configuration); //代表可以访问所有的资源
return source;
}
重写 OncePerRequestFilter 用来查看请求头中是否携带有Token令牌 有就生成 anthentication(后面就不需要再验证了) 如果没有的话就放行 进入 UsernamePasswordAuthenticationFilter 进行用户名密码 校验
@Component
public class CumtomAuthorizeFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Autowired
private UserDetailsServiceImpl userDetailsService;
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
//通过 getAuthentication 看是否生成 authentication 如果有的话 就放入context中 后面就不需要验证了
//返回null 就放行 进入 UsernamePasswordAuthenticationFilter 进行用户名密码 校验
//第一次登陆 肯定没有token 直接走 UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationToken authentication=getAuthentication(request);
if(authentication!=null){
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
//查看请求头是否有 token
String token = request.getHeader("token");
if(!StringUtils.isEmpty(token)){
//通过jwt工具类 返回用户名
String username = JwtUtils.getUsernameToken(token);
//从redis中取出 路径字符串 转换为 权限列表
List<String> permissons = (List<String>) redisTemplate.opsForValue().get(username);
Collection<GrantedAuthority> authorities=new ArrayList<>();
for (String permisson : permissons) {
if(StringUtils.isEmpty(permisson)){
continue;
}
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permisson);
authorities.add(authority);
}
if(!StringUtils.isEmpty(username)){
//前面都成立的话 加载用户的详细信息 生成 UsernamePasswordAuthenticationToken 令牌
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
return new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
}
return null;
}
return null;
}
}
认证成功处理器
第一个功能 将用户名 和 用户权限(路径) 放入redis中 上面的filter 就可以通过用户名去redis中取出来了
第二个功能 通过response对象 将token返回 回前台 再请求的时候 前台回在请求头中携带这个token 在上面的filter 中直接生成 authentication (认证主体)
@Component
public class CustomSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
CumtomUserDetails userDetails = (CumtomUserDetails) authentication.getPrincipal();
String token = JwtUtils.createToken(userDetails.getUsername());
redisTemplate.opsForValue().set(userDetails.getUsername(),userDetails.getPermissons());
ResponeUtils.out(response, R.ok().message("登陆成功").data("token",token));
}
}
认证失败处理器
拦截各种类型的异常 不细分的话 直接 ResponeUtils.out(response, R.error());
@Component
public class CustomFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
if(exception instanceof InternalAuthenticationServiceException){
ResponeUtils.out(response, R.error().message("用户不存在"));
}
if(exception instanceof BadCredentialsException){
ResponeUtils.out(response, R.error().message(exception.getMessage()));
}
if(exception instanceof UsernameNotFoundException){
ResponeUtils.out(response, R.error().message("用户不存在"));
}
if(exception instanceof DisabledException){
ResponeUtils.out(response, R.error().message("用户被禁用"));
}
if(exception instanceof LockedException){
ResponeUtils.out(response, R.error().message("账户锁定"));
}
if(exception instanceof AccountExpiredException){
ResponeUtils.out(response, R.error().message("账户过期"));
}
if(exception instanceof CredentialsExpiredException){
ResponeUtils.out(response, R.error().message("证书过期"));
}
else {
ResponeUtils.out(response, R.error().message("其他错误"));
}
}
}
成功退出 处理器
主要是将redis中的数据给清理掉 然后返回JSON数据就行了
@Component
public class CustomLayoutHandler implements LogoutHandler {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String token = request.getHeader("token");
System.out.println(token);
if(token!=null){
String username = JwtUtils.getUsernameToken(token);
System.out.println(username);
redisTemplate.delete(username);
}
try {
new ObjectMapper().writeValue(response.getOutputStream(), R.ok().message("退出成功"));
} catch (IOException e) {
ResponeUtils.out(response,R.error());
}
}
}
未登陆处理器 AuthenticationEntryPoint
@Component
public class CustomAuthorizationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ResponeUtils.out(response,R.error().message("请登陆再访问"));
}
}
登陆了 但是权限不够 AccessDeniedHandler
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ResponeUtils.out(response, R.error().message("无权访问"));
}
}
自定义权限控制器
通过 getPrincipal() 从authentication 取出主体 其实就是前面OncePerRequestFilter 中存入UsernamePasswordAuthenticationToken中的
UserDetails, 然后取出里面的路径字符串 通过遍历和请求的url比较 里面有的话 就返回true表示有权限 返回false就表示没有权限
@Component("rbacService")
public class MyRbacService {
public boolean hasPermission(HttpServletRequest request, Authentication authentication){
Object principal = authentication.getPrincipal();
/*if(request.getRequestURI().indexOf("admin")==-1){
return true;
}*/
if(principal instanceof UserDetails){
CumtomUserDetails userDetails=(CumtomUserDetails) principal;
boolean flag=false;
List<String> permissons = userDetails.getPermissons();
for (String permisson : permissons) {
if(request.getRequestURI().equals(permisson)){
flag= true;
break;
}
}
return flag;
}
return false;
}
}