spring security配置
userDetailsService 是自定义验证账号密码的业务
- 配置路径请求权限
- 配置安全相关的filter
- 注入密码加密的bean BCryptPasswordEncoder
- 配置验证用户身份的service和加密方式
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
/**
* 自定义用户认证逻辑
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 认证失败处理类
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 退出处理类
*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* token认证过滤器
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 跨域过滤器
*/
@Autowired
private CorsFilter corsFilter;
/**
* 允许匿名访问的地址
*/
@Autowired
private PermitAllUrlProperties permitAllUrl;
/**
* 解决 无法直接注入 AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
// 注解标记允许匿名访问的url
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 禁用HTTP响应标头
.headers().cacheControl().disable().and()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/v1/login", "/register", "/captchaImage").permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
// 添加Logout filter
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加CORS filter
// httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
// httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
UserDetailsServiceImpl 实现类
返回用户对象,包含前端传入的账号和前端传入的密码进行配置加密方法加密的密码
@Service
public class UserDetailsServiceImpl implements UserDetailsService
{
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException
{
// 根据用户名查数据库,获取密码和用户信息
SysUser userReturn = new SysUser();
userReturn.setUserId(1111L);
userReturn.setDeptId(1L);
userReturn.setUserName(userName);
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
userReturn.setPassword( passwordEncoder.encode("password") );
return createLoginUser(userReturn);
}
public UserDetails createLoginUser(SysUser user)
{
return new LoginUser(user.getUserId(), user.getDeptId(), user, new HashSet<String>());
}
}
登录控制层
在SecurityConfig配置免鉴权的路径地址 “/v1/login”
传入账号和MD5加密的密码
前缀+生成的登录UID,作为redis的key值,储存LoginUser ,加上限制时间
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
根据生成的登录UID,进行JWT加密返回前端作为head里的 Bear Token
@RestController
@RequestMapping("/v1")
@Api(value = "登录", tags = {"登录"})
public class LoginController {
@Resource
private AuthenticationManager authenticationManager;
@ApiOperation(value = "login登录")
@PostMapping("/login")
public R login(@RequestBody WxCode wxCode) throws Exception {
String name ="name";
String password="password";
// 用户验证
Authentication authentication = authenticationManager.authenticate
(new UsernamePasswordAuthenticationToken(name, password));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 生成token
Map<String, Object> claims = new HashMap<>();
claims.put("login_user_key", UUID.fastUUID().toString());
String tokenSecret = "abcdefghijklmnopqrstuvwxyz";
// token 存 redis
String token = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512,tokenSecret ).compact();
return R.ok(token);
}
}
pom里引入的jar
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.14</version>
</parent>
<!-- JWT里需要 -->
<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>
<!-- Token生成与解析-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
JwtAuthenticationTokenFilter
在SecurityConfig配置jwt验证过滤器
解密head里的token得到 登录UID
UID加上前缀去找redis找登录用户,没找到说明token过期了,
找到说明token有效,登录状态有效
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter{
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException
{
String token = request.getHeader("Authorization");
if (StringUtils.isNotEmpty(token) && token.startsWith("Bearer "))
{
token = token.replace("Bearer ", "");
}
if (StringUtils.isNotEmpty(token))
{
Claims claims= Jwts.parser().setSigningKey("abcdefghijklmnopqrstuvwxyz")
.parseClaimsJws(token).getBody();
String uuid = (String) claims.get("login_user_key");
System.out.println(uuid);
// 验证token令牌有效期,相差不足20分钟,自动刷新缓存, token存入redis里,过期需要重新获取token
SysUser user = new SysUser();
// user.setUserName("wx766e6c996b1542d2");
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
// 加密不加密,貌似都行
// String encoderPassword =passwordEncoder.encode("wx766e6c996b1542d2");
// user.setPassword(encoderPassword);
user.setPassword(null);
LoginUser loginUser= new LoginUser();
loginUser.setUser(user);
UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
// 下面代码注释掉,不会影响鉴权
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 下面代码表示鉴权通过,哪怕上面 user setUserName和setPassword 都是空
// SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(null, null, null));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
System.out.println("开始验证head里的token");
chain.doFilter(request, response);
}
}
AuthenticationEntryPointImpl
config里配置的认证失败处理类 返回未授权
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
{
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
throws IOException
{
String msg = StrUtil.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(JSON.toJSONString(R.build(String.valueOf(HttpStatus.UNAUTHORIZED.value()),msg)));
log.error("认证失败。。。。。。。。。");
}
}