一、spring-boot引入SpringSecurity
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
二、因为springboot的自动装配特性
因为springboot有自动装配的特性,所以,会自动实现导入包里的内容,在访问地址加端口号时会自动产生一个页面进行登录,
默认账号为:user
默认密码为控制台输出的内容
三、通过数据库自定义登录账号
显然默认配置是无法满足我们日常的使用,所以我们需要自定义,通过分析得到UserDetailsService是在登录时候获取用户的类,所以我们只需要将其自定义以下就可以。
1.在service.impl包下创建一个UserDetailsServiceImpl类继承UserDetailsService接口并实现其中的loadUserByUsername方法就可以用来调用自己数据库的账号
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserDAO userDAO; // user的mapper,用来调用数据库sql
@Autowired
private MenuDAO menuDAO; // menu的mapper,用来调用数据库sql
@Override
public UserDetails loadUserByUsername(String useName) throws UsernameNotFoundException {
// 查询用户信息
User user = userDAO.selectByUserName(useName);
// 没有查询到用户抛出异常
if (Objects.isNull(user)){
throw new RuntimeException("当前不存在该用户名,请注册!");
}
// 查询对应的权限信息
List<String> perms = menuDAO.selectPermsByUserId(user.getId());
// 把数据封装成UserDetails返回
return new LoginUser(user, perms);
}
}
2.在上一步的的实现方法中发现我们需要返回一个UserDetails对象,该对象内包含我们的用户信息,所以只需要创建一个登录对象LoginUser实现UserDetails接口
public class LoginUser implements UserDetails {
public LoginUser() {
}
public LoginUser(User user, List<String> perms) {
this.user = user;
this.perms = perms;
}
private User user; // 存入用户信息
private List<String> perms; // 存入权限信息
private List<GrantedAuthority> authorities; // 存入SpringSecurity所需要类型的权限信息,需要通过下面的流转换
/**
* 获取权限信息
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (authorities != null){
return authorities;
}
authorities = perms.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return authorities;
}
/**
* 这里必须使用自己的用户类中的getPassword()方法,不然会出现 Empty encoded password 错误
* @return
*/
@Override
public String getPassword() {
return user.getPassword();
}
@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;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public List<String> getPerms() {
return perms;
}
public void setPerms(List<String> perms) {
this.perms = perms;
}
public void setAuthorities(List<GrantedAuthority> authorities) {
this.authorities = authorities;
}
}
到这里就实现了从数据库里获取用户信息。
3.到这里运行后发现用自己的用户名和密码是不能够登录上去的,因为SpringSecurity里自动加入了加密密码的功能.
3.1如果想要使用明文存储密码,则需要在明文上存储前加入{noop},如{noop}123456,123456就是我们的密码了,
3.2 如果想使用SpringSecurity的加密密码方法,可以在config包下创建一个SpringConfig类进行配置SpringSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启权限注解,默认是关闭的
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 加密方式
* 创建BCryptPasswordEncoder 注入容器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
3.3 到此,我们就实现了将自己的用户名和密码登录,不过,这个密码必须先进行加密后存入数据库才行,使用以下语句获取到加密后的密码
@Autowired
PasswordEncoder passwordEncoder;
@Test
public void passwordEncoder() {
// 加密
String encode = passwordEncoder.encode("123456");
System.out.println(encode);
}
4.使用的自己的登录页面
4.1 先使用Controller写一个登录登出接口
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
/**
* 登录
* @return
*/
@PostMapping("/user/login")
public ResponseResult login(@RequestBody User user) {
return loginService.login(user);
}
/**
* 退出登录
* @return
*/
@RequestMapping("/user/logout")
public ResponseResult logout() {
return loginService.logout();
}
}
4.2 实现该接口
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Override
public ResponseResult login(User user) {
// 1. UsernamePasswordAuthenticationToken创建 Authentication对象
// 传入 AuthenticationManager authenticate 用户认证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 认证没通过,给出提示
if (Objects.isNull(authenticate)){
throw new RuntimeException("登陆失败");
}
// 认证通过,使用userid生成一个jwt jwt存入 ResponseResult 返回
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userId);
// map=token:jwt
Map<String, String> jwtMap = new HashMap<>();
jwtMap.put("token", jwt);
// 把完整的用户信息存入redis userid作为key
redisCache.setCacheObject("token:" + userId, jwt); // token存入redis中
redisCache.setCacheObject("login:" + userId, loginUser); // 登录的对象存入redis中
return new ResponseResult(200, "登陆成功", jwtMap);
}
@Override
public ResponseResult logout() {
// 获取 SecurityContextHolder 中的用户id
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long userId = loginUser.getUser().getId();
// 删除 redis 中的值
redisCache.deleteObject("token:" + userId);
redisCache.deleteObject("login:" + userId);
return new ResponseResult(200, "注销成功");
}
}
4.3 到此,我们就实现了登录登出自己的登录接口
4.4 当我们已经处于登录状态时,请求发送过来会在请求头中带有token,我们将该token解析得到userId,到redis数据库内查找该token,如果token一致则说明是已经登录的用户,则会通过,并将获取权限信息封装到Authentication中,告诉后面的调度,该请求是已经登录的,若请求头没有带有token或token有误,则会直接跳过。代码如下:JwtAuthenticationTokenFilter
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 需要前端在请求头中加入token 获取 token
String token = httpServletRequest.getHeader("token");
if (!StringUtils.hasText(token)) { // token为空放行
filterChain.doFilter(httpServletRequest, httpServletResponse);// 对请求放行
return;
}
// 解析 token
String userId;
try {
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
filterChain.doFilter(httpServletRequest, httpServletResponse);// 对请求放行
return;
}
// 从redis中获取token
String redisToken = redisCache.getCacheObject("token:" + userId);
if (!token.equals(redisToken)){
filterChain.doFilter(httpServletRequest, httpServletResponse);// 对请求放行
return;
}
// 从redis中获取用户信息
String redisKey = "login:" + userId;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if (Objects.isNull(loginUser)){
filterChain.doFilter(httpServletRequest, httpServletResponse);// 对请求放行
return;
}
// 能访问到这里才是携带token访问成功
// 存入SecurityContextHolder
// 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 放行
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
4.5 当我们实现JwtAuthenticationTokenFilter 的时候,发现并没有用,需要将其注入到SecurityConfig中
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启权限注解,默认是关闭的
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* 加密方式
* 创建BCryptPasswordEncoder 注入容器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 重写
* @return
* @throws Exception
*/
@Bean // 重写该方法来暴露 AuthenticationManager
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 配置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// http
// //关闭csrf
// .csrf().disable()
// //不通过Session获取SecurityContext
// .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// .and()
// .authorizeRequests()
// // 对于登录接口 允许匿名访问
// .antMatchers("/user/login").anonymous()
// // 除上面外的所有请求全部需要鉴权认证
// .anyRequest().authenticated();
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// permitAll 登不登陆都放行
.antMatchers("/hello").permitAll()
// 匿名可以访问
.antMatchers("/user/login").anonymous()
.anyRequest().authenticated();
// 添加过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 添加过滤器
}
}
5.配置异常处理
当我们认证和授权时候出现异常的时候,想要返回一个json格式的字符串给前端,这时候就需要下面的配置
5.1 AuthenticationEntryPointImpl 和 AccessDeniedHandlerImpl
5.1.1 AuthenticationEntryPointImpl为了 当授权时出现异常,捕获并返回异常json
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
// 当授权时出现异常,捕获并返回异常json
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
// 处理异常
ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "您的权限不足");
String json = JSON.toJSONString(result);
WebUtils.renderString(httpServletResponse, json);
}
}
5.1.2 AccessDeniedHandlerImpl 当认证时段出现异常,则会捕获异常,返回异常json
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
// 当认证时段出现异常,则会捕获异常,返回异常json
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
// 处理异常
ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "用户认证失败,请重新登录");
String json = JSON.toJSONString(result);
WebUtils.renderString(httpServletResponse, json);
}
}
5.1.3 将他们注入到 SecurityConfig中
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启权限注解,默认是关闭的
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private AccessDeniedHandler accessDeniedHandler;
/**
* 加密方式
* 创建BCryptPasswordEncoder 注入容器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 重写
* @return
* @throws Exception
*/
@Bean // 重写该方法来暴露 AuthenticationManager
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 配置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// http
// //关闭csrf
// .csrf().disable()
// //不通过Session获取SecurityContext
// .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// .and()
// .authorizeRequests()
// // 对于登录接口 允许匿名访问
// .antMatchers("/user/login").anonymous()
// // 除上面外的所有请求全部需要鉴权认证
// .anyRequest().authenticated();
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// permitAll 登不登陆都放行
.antMatchers("/hello").permitAll()
// 匿名可以访问
.antMatchers("/user/login").anonymous()
.anyRequest().authenticated();
// 添加过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 添加过滤器
// 配置异常处理器
http.exceptionHandling()
// 认证失败处理器
.authenticationEntryPoint(authenticationEntryPoint)
// 授权失败处理器
.accessDeniedHandler(accessDeniedHandler);
// 允许跨域
http.cors();
}
}
6.跨域问题
6.1 得先实现springboot的跨域,如下
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
// .allowedOriginPatterns("*") // 版本不对2.1.4没有
// .allowedOrigins("*") // 自己加的,应该不能替代上面那个
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
6.2 开启SecurityConfig内的跨域配置
// 允许跨域
http.cors();
7.自定义权限注解
7.1自定义
@Component("ex")
public class ExpressionRoot {
public boolean hasAuthority(String authority){
// 获取用户的权限
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
List<String> perms = loginUser.getPerms();
// 判断用户权限集合中是否存在authority
return perms.contains(authority);
}
}
7.2 使用自定义的权限注解
@PreAuthorize("@ex.hasAuthority('system:dept:list')") // 自定义的