【Java雜貨鋪】用Security作權限極簡入門

原來大多數單體項目都是用的shiro,隨着分佈式的逐漸普及以及與Spring的天生天然的結合。Spring Security安全框架越受你們的青睞。本文會教你用SpringSecurity設計單項目的權限,關於如何作分佈式的權限,後續會跟進。html

爲何選擇SpringSecurity?

現現在,在JavaWeb的世界裏Spring能夠說是一統江湖,隨着微服務的到來,SpringCloud能夠說是Java程序員必須熟悉的框架,就連阿里都爲SpringCloud寫開源呢。(好比大名鼎鼎的Nacos)做爲Spring的親兒子,SpringSecurity很好的適應了了微服務的生態。你能夠很是簡便的結合Oauth作認證中心服務。本文先從最簡單的單體項目開始,逐步掌握Security。更多可達官方文檔前端

準備

我準備了一個簡單的demo,具體代碼會放到文末。提早聲明,本demo沒有用JWT,由於我想把token的維護放到服務端,更好的維護過時時間。(固然,若是未來微服務認證中心的形式,JWT也能夠作到方便的維護過時時間,不作過多討論)若是想了解Security+JWT簡易入門,請戳git

本項目結構以下程序員

另外,本demo使用了MybatisPlus、lombok。github

核心代碼

首先須要實現兩個類,一個是UserDetails的實現類SecurityUser,一個是UserDetailsService的實現類SecurityUserService。spring

**
 * Security 要求須要實現的User類
 * */
@Data
public class SecurityUser implements UserDetails {
    @Autowired
    private SysRoleService sysRoleService;
    //用戶登陸名(注意此處的username和SysUser的loginName是一個值)
    private String username;
    //登陸密碼
    private String password;
    //用戶id
    private SysUser sysUser;
    //該用戶的全部權限
    private List<SysMenu> sysMenuList;
    /**構造函數*/
    public SecurityUser(SysUser sysUser){
        this.username = sysUser.getLoginName();
        this.password = sysUser.getPassword();
        this.sysUser = sysUser;
    }
    public SecurityUser(SysUser sysUser,List<SysMenu> sysMenuList){
        this.username = sysUser.getLoginName();
        this.password = sysUser.getPassword();
        this.sysMenuList = sysMenuList;
        this.sysUser = sysUser;
    }
    /**須要實現的方法*/
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for(SysMenu menu : sysMenuList) {
            authorities.add(new SimpleGrantedAuthority(menu.getPerms()));
        }
        return authorities;
    }
    @Override
    public String getPassword() {
        return this.password;
    }
    @Override
    public String getUsername() {
        return this.username;
    }
    //默認帳戶未過時
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    //默認帳戶沒有帶鎖
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    //默認憑證沒有過時
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    //默認帳戶可用
    @Override
    public boolean isEnabled() {
        return true;
    }
}

複製代碼

這個類包含着某個請求者的信息,在Security中叫作主體。其中這個方法是必須實現的,能夠獲取用戶的具體權限。咱們這邊權限的顆粒度達到了菜單級別,而不是不少開源項目中角色那級別,我以爲顆粒度越細越方便(我的以爲...)數據庫

/**
 * Security 要求須要實現的UserService類
 * */
@Service
public class SecurityUserService implements UserDetailsService{

    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysMenuService sysMenuService;
    @Autowired
    private HttpServletRequest httpServletRequest;

    @Override
    public SecurityUser loadUserByUsername(String loginName) throws UsernameNotFoundException {
        LambdaQueryWrapper<SysUser> condition = Wrappers.<SysUser>lambdaQuery().eq(SysUser::getLoginName, loginName);
        SysUser sysUser = sysUserService.getOne(condition);
        if (Objects.isNull(sysUser)){
            throw new UsernameNotFoundException("未找到該用戶!");
        }
        Long projectId = null;
        try{
            projectId = Long.parseLong(httpServletRequest.getHeader("projectId"));
        }catch (Exception e){

        }
        SysMenuModel sysMenuModel;
        if (sysUser.getUserType()){
            sysMenuModel = new SysMenuModel();
        }else {
            sysMenuModel = new SysMenuModel().setUserId(sysUser.getId());
        }
        sysMenuModel.setProjectId(projectId);
        List<SysMenu> menuList = sysMenuService.getList(sysMenuModel);
        return new SecurityUser(sysUser,menuList);
    }
}
複製代碼

顯而易見,這個類實現了惟一的方法loadUserByUsername,從而能夠拿到某用戶的全部權限,並生成主體,在後面的filter中就能夠見到他的做用了。後端

在看配置和filter以前,還有一個類須要說明一下,此類提供方法,可讓用戶未登陸、或者token失效的狀況下進行統一返回。緩存

@Component
public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {

    private static final long serialVersionUID = 1L;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"token失效,請登錄後重試");
    }
}
複製代碼

ok,接下來看配置,實現了WebSecurityConfigurerAdapter的SecurityConfig類,特別說明,本demo算是先後端分離的前提下寫的,因此實現過多的方法,其實這個類能夠實現三個方法,具體請戳安全

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter{

    @Autowired
    SecurityAuthenticationEntryPoint securityAuthenticationEntryPoint;
    @Autowired
    SecurityFilter securityFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //禁止csrf
                .csrf().disable()
                //異常處理
                .exceptionHandling().authenticationEntryPoint(securityAuthenticationEntryPoint).and()
                //Session管理方式
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                //開啓認證
                .authorizeRequests()
                .antMatchers("/login/login").permitAll()
                .antMatchers("/login/register").permitAll()
                .antMatchers("/login/logout").permitAll()
                .anyRequest().authenticated();
        http
                .addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class);
    }
}
複製代碼

異常處理就是上面那個類,Session那幾種管理方式我在那篇Security+JWT的文章中也有所講解,比較簡單,而後是幾個不用驗證的登陸路徑,剩下的都須要通過咱們下面這個filter。

@Slf4j
@Component
public class SecurityFilter extends OncePerRequestFilter {

    @Autowired
    SecurityUserService securityUserService;
    @Autowired
    SysUserService sysUserService;
    @Autowired
    SysUserTokenService sysUserTokenService;

    /**
     * 認證受權
     * */
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
                                    FilterChain filterChain) throws ServletException, IOException {
        log.info("訪問的連接是:{}",httpServletRequest.getRequestURL());
        try {
            final String token = httpServletRequest.getHeader("token");
            LambdaQueryWrapper<SysUserToken> condition = Wrappers.<SysUserToken>lambdaQuery().eq(SysUserToken::getToken, token);
            SysUserToken sysUserToken = sysUserTokenService.getOne(condition);
            if (Objects.nonNull(sysUserToken)){
                SysUser sysUser = sysUserService.getById(sysUserToken.getUserId());
                if (Objects.nonNull(sysUser)){
                    SecurityUser securityUser = securityUserService.loadUserByUsername(sysUser.getLoginName());
                    //將主體放入內存
                    UsernamePasswordAuthenticationToken authentication =
                            new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
                    //放入內存中去
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }catch (Exception e){
            log.error("認證受權時出錯:{}", Arrays.toString(e.getStackTrace()));
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}
複製代碼

判斷用戶是否登陸,就是從數據庫中查看是否有未過時的token,若是存在,就把主體信息放進到項目的內存中去,特別說明的是,每一個請求鏈結束,SecurityContextHolder.getContext()的數據都會被clear的,因此,每次請求的時候都須要set。

以上就完成了Security核心的建立,爲了業務代碼方便獲取內存中的主體信息,我特地加了一個獲取用戶信息的方法

/**
 * 獲取Security主體工具類
 * @author pjjlt
 * */
public class SecurityUserUtil {
    public static SysUser getCurrentUser(){
        SecurityUser securityUser = (SecurityUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if (Objects.nonNull(securityUser) && Objects.nonNull(securityUser.getSysUser())){
            return securityUser.getSysUser();
        }
        return null;
    }
}
複製代碼

業務代碼

以上是Security核心代碼,下面簡單加兩個業務代碼,好比登陸和某個接口的權限訪問測試。

萬物之源登陸登出

首先,不被filter攔截的那三個方法註冊、登陸、登出,我都寫在了moudle.controller.LoginController這個路徑下,註冊就不用說了,就是一個insertUser的方法,作好判斷就好,密碼經過AES加個密。

下面看下登陸代碼,controller層就不說了,反正就是個驗參。

/**
     * 登陸,返回登陸信息,前端須要緩存
     * */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public JSONObject login(SysUserModel sysUserModel) throws Exception{
        JSONObject result = new JSONObject();
        //1. 驗證帳號是否存在、密碼是否正確、帳號是否停用
        Wrapper<SysUser> sysUserWrapper = Wrappers.<SysUser>lambdaQuery()
                .eq(SysUser::getLoginName,sysUserModel.getLoginName()).or()
                .eq(SysUser::getEmail,sysUserModel.getEmail());
        SysUser sysUser = baseMapper.selectOne(sysUserWrapper);
        if (Objects.isNull(sysUser)){
            throw new Exception("用戶不存在!");
        }
        String password = CipherUtil.encryptByAES(sysUserModel.getPassword());
        if (!password.equals(sysUser.getPassword())){
            throw new Exception("密碼不正確!");
        }
        if (sysUser.getStatus()){
            throw new Exception("帳號已刪除或已停用!");
        }
        // 2.更新最後登陸時間
        sysUser.setLoginIp(ServletUtil.getClientIP(request));
        sysUser.setLoginDate(LocalDateTime.now());
        baseMapper.updateById(sysUser);
        // 3.封裝token,返回信息
        String token = UUID.fastUUID().toString().replace("-","");
        LocalDateTime expireTime = LocalDateTime.now().plusSeconds(expireTimeSeconds);
        SysUserToken sysUserToken = new SysUserToken()
                .setToken(token).setUserId(sysUser.getId()).setExpireTime(expireTime);
        sysUserTokenService.save(sysUserToken);
        result.putOpt("token",token);
        result.putOpt("expireTime",expireTime);
        return result;
    }
複製代碼

首先驗證下用戶是否存在,登陸密碼是否正確,而後封裝token,值得一提的是,我並無從數據庫(sysUserToken)中獲取用戶已經登陸的token,而後更新過時時間的形式作登陸,而是每次登陸都獲取新token,這樣就能夠作到多端登陸了,後期還能夠作帳號登陸數量的控制。

而後就是登出,刪除庫中存在的token

/**
     * 登出,刪除token
     * */
    @Override
    public void logout() throws Exception{
        String token = httpServletRequest.getHeader("token");
        if (Objects.isNull(token)){
            throw new LoginException("token不存在",ResultEnum.LOGOUT_ERROR);
        }
        LambdaQueryWrapper<SysUserToken> sysUserWrapper = Wrappers.<SysUserToken>lambdaQuery()
                .eq(SysUserToken::getToken,token);
        baseMapper.delete(sysUserWrapper);
    }
複製代碼

權限驗證

這邊我維護了兩個帳號,一個是超級管理員majian,擁有全部權限。一個是普通人員_pjjlt,只有一些權限,咱們看一下訪問接口的效果。

咱們訪問的接口是moudle.controller.LoginController路徑下的

@PreAuthorize("hasAnyAuthority('test')")
@GetMapping("test")
public String test(){
    return "test";
}
複製代碼

其中hasAnyAuthority('test')就是權限碼

咱們模擬用不一樣帳號訪問,就是改變請求header中的token值,就是登陸階段返回給前端的token。

首先是超級管理員驗證

而後是普通管理員訪問

接着沒有登陸(token不存在或者已過時)訪問

結束語

本文簡單講解了,主要是將Security相關的東西,具體實現角色的三要素,用戶、角色、權限(菜單)能夠看個人代碼,都寫完測完了,原本想寫個文檔管理系統,幫助我司更好的管理接口文檔,but有位小夥伴找了一個不錯的開源的了,因此這代碼就成了個人一個小demo。 最後的最後,可不能夠放波公衆號啊,之後打算公衆號同步寫文章。

demo地址

github.com/majian1994/…

相關文章
相關標籤/搜索