目前主流的权限框架为 Apache的Shiro以及Spring的Security,本文描述的是SpringBoot与Security的整合,可以说Security的整合与Spring的框架基本是十分简单的,毕竟是同一家的产品,哈哈。
本文基于SpringBoot脚手架,采用纯Java方式来配置Security,告别繁琐的XML配置。
吹理论这个东西我也不太擅长,大家百度也是一大堆,我就从实战出发,直接用编码的方式,来让大家更直观的学习Security的这个框架。
首先,导入Security与SpingBoot的Maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
其实Security最重要的几个点就是几个过滤器和Security的一个上下文。
在使用过程中我们会发现我们再输入用户密码账号的时候会发现我们并没有对密码进行加密处理,SpringSecurity却能够将我们明文的密码与加密的密码对上。
你可以理解为就是Securiy将一个用户信息查询出来是,会将我们的一个用户名和密码去匹配,会将这个密码再次加密一遍,然后进行一个值的比较,如果加密的值一致就会认为是正确的,这一块我们也可以使用自带的几种方式,也可以自定义我们的一个加密方式:
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
本文是使用的一个框架自带的密码加密,你可以实现PasswordEncoder这个接口进行自定义
接下来,我们可以预先实现一些,我们自定义的一些处理,诸如未登录,登录成功,登录失败,退出成功的Hnadler
ServerCallBack是自定义的一个返回工具,你们可以自己实现。
@Component
public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
Map<String,Object> json = new HashMap<>(2);
json.put("code",401);
json.put("msg","未登录");
ServerCallBack.writeServerMessage(httpServletResponse, ServerReturnMap.getServerMsg(401,"未登录",null,null));
}
}
@Component
public class SecurityAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
ServerCallBack.writeServerMessage(httpServletResponse, ServerReturnMap.getServerMsg(400,"登录失败","content",e.getMessage()));
}
}
@Component
public class SecurityAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
Map<String,Object> param=new HashMap<>();
param.put("user", (User)authentication.getPrincipal());
ServerCallBack.writeServerMessage(httpServletResponse, ServerReturnMap.getServerMsg(200,"登录成功","token",JwtFactory.createJWT(param)));
}
}
这是一个登出Handler
@Component
public class SecurityLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
ServerCallBack.writeServerMessage(httpServletResponse,"");
}
}
无权限处理Handler
@Component
public class SecurityAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
//业务处理
}
}
这边有一个注意点,如果说你有设置一个全局异常的一个Exception的拦截的话,这个AccessDeniedException 是不会爆出来的,这个坑我当时研究了一段时间,是因为你的统一异常处理去把他拦截了,所有我们可以在统一异常处理的时候判断异常的类型,然后根据这个AccessDeniedException 异常进行相应的处理。
还有一个关键点就是我们每次请求的话该如何进行一个请求接口的一个鉴权呢这边的话也很简单只需要实现我们的一个接口即可
@Component
public class ActivityFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String requestURI = httpServletRequest.getRequestURI();
//除登录路径外,均需要校验token参数
if(requestURI.equals("/system/user/login")){
doFilter(httpServletRequest,httpServletResponse,filterChain);
}else {
String token= httpServletRequest.getHeader("token");
if (token==null||token.equals("")) {
ServerCallBack.writeServerMessage(httpServletResponse,"禁止访问");
return;
}else {
//这里的我是使用了JWT,好兄弟们可以根据自己的一个业务进行不同的处理
Claims claims= JwtFactory.encodeJwt(token);
Object obj = claims.get("user");
User user = JSON.parseObject(JSONObject.toJSONString(obj, true),User.class);
//这边的话是实例一个我们的用户密码token认证,将我们jwt中的一个用户类,和权限集合放进去即可,会
UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
//然后只需要这个处理丢进我们的一个Security上下文中,就可以起到权限拦截的效果了
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
doFilter(httpServletRequest,httpServletResponse,filterChain);
}
}
}
然后我们就只需要将我们的用户类去实现Security的一个UserDetails接口即可,如果你觉得可读性不好的话,可以做其它的相应处理
@Getter
@Setter
public class User implements UserDetails, Serializable {
private String userId;
private String account;
private String password;
private String phone;
private String email;
private List<Role> roles;
private String isEnabled; //是否激活/禁用
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(roles.size()<1){
throw BusinessException.baseException(403,"尚未配置角色");
}
List<GrantedAuthority> authorities=new ArrayList<>();
roles.stream().forEach(c->authorities.add(new SimpleGrantedAuthority(c.getRoleTag())));
return authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.account;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return this.getIsEnabled().equals("正常")?true:true;
}
}
具体的一个参数说明的话,搬砖的小伙伴们可以自行百度一下,加深自己的理解。
接下来就是我们的一个重头戏,就是我们比较关键的一个用户账号密码与我们数据库中的一个用户密码进行匹配的时候回了,这个时候我们需要实现他的一个接口
@Component
public class CustomerUserDetails implements UserDetailsService {
@Resource
private UserDao userDao;
/**
* 通过用户传入用户名(账号)进行查询
* @param account
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
//这里就是我们自己的一个业务逻辑,通过账号获取我们的用户名,让后将用户对象返回回去
User user = userDao.loginUser(account);
return user;
}
}
最后的话我们要将这些设置做一个规整,将这些配置进行生效:
@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启Security注解
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//登录失败处理
@Autowired
private SecurityAuthenticationFailureHandler securityAuthenticationFailureHandler;
//未登录处理
@Autowired
private SecurityAuthenticationEntryPoint securityAuthenticationEntryPoint;
//登录成功处理
@Autowired
private SecurityAuthenticationSuccessHandler securityAuthenticationSuccessHandler;
//登出成功处理
@Autowired
private SecurityLogoutSuccessHandler securityLogoutSuccessHandler;
@Autowired
private CustomerUserDetails customerUserDetails;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private ActivityFilter activityFilter;
@Autowired
private SecurityAccessDeniedHandler securityAccessDeniedHandler;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//指定用户处理Service并指定加密方式加密
auth.userDetailsService(customerUserDetails).passwordEncoder(passwordEncoder);
}
/**
* security 配置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling().authenticationEntryPoint(securityAuthenticationEntryPoint)//设置未登录信息
.and().authorizeRequests().antMatchers("/system/user/login").permitAll().anyRequest().authenticated()
.and().exceptionHandling().accessDeniedHandler(securityAccessDeniedHandler)//该路径的权限是所有的可以访问,其他路径都需要认证
.and().formLogin().loginProcessingUrl("/system/user/login").usernameParameter("account").passwordParameter("password")//指定表单登录为自定义登录地址。设置用户名和密码的参数;
.successHandler(securityAuthenticationSuccessHandler).failureHandler(securityAuthenticationFailureHandler)//指定登录失败和登录成功处理器
.and().logout().logoutUrl("/system/user/loginOut").logoutSuccessHandler(securityLogoutSuccessHandler);//指定登出路径和登出成功处理器
http.addFilterBefore(activityFilter, UsernamePasswordAuthenticationFilter.class);//指定这个filter作用在用户名密码处理拦截器之前。
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
好了,基本的一个Security的大致使用就是这样了,大家就可以在自己的Controller进行一个权限注解即可了,如果有描述错误的地方请大家多多包涵,我也不太喜欢长篇大论,原理方面我就不讲了,有的好兄弟可能比我还要了解,我呢就是以实战为主,直接上代码,让大家先能简单上手进行一个简单的使用,后续的一些优化和更高级的用法,可以交由好兄弟们直接去扩展哦,谢谢大家,有任何疑问,欢迎大家在评论区提问,谢谢。